mihomo-cli 1.2.5 → 1.3.1

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/index.js CHANGED
@@ -1,16 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // 内置模块
3
4
  const path = require('path');
4
- const { spawn } = require('child_process');
5
+ const { spawn, exec } = require('child_process');
6
+ const { promisify } = require('util');
7
+ const readline = require('readline');
5
8
 
9
+ // 第三方模块
10
+ // (无第三方模块依赖)
11
+
12
+ // 本地模块
6
13
  const config = require('./src/config');
7
14
  const kernel = require('./src/kernel');
8
15
  const subscription = require('./src/subscription');
9
- const processMgr = require('./src/process');
16
+ const processManager = require('./src/process');
10
17
  const overwrite = require('./src/overwrite');
11
18
  const utils = require('./src/utils');
12
19
 
13
- const VERSION = require('./package.json').version;
20
+ const execAsync = promisify(exec);
21
+ const VERSION = utils.VERSION;
22
+ const { colors } = utils;
14
23
 
15
24
  const UI_URLS = {
16
25
  zash: 'https://board.zash.run.place',
@@ -49,20 +58,28 @@ process.on('unhandledRejection', reason => {
49
58
  });
50
59
 
51
60
  function printShortHelp() {
52
- console.log('\nmihomo-cli v' + VERSION + ' (mihomo help 查看完整帮助)\n');
61
+ console.log('\n' + colors.cyan(colors.bold('mihomo-cli v' + VERSION)) + ' (mihomo help 查看完整帮助)\n');
53
62
  console.log(
54
63
  '常用命令:\n' +
55
- ' start [tun|mixed] 启动/切换代理\n' +
56
- ' ui [zash|dash|yacd] 打开 Web UI\n' +
57
- ' ow [on|off] 覆写配置\n' +
58
- ' sub [use|update] 订阅管理\n',
64
+ ' ' +
65
+ colors.bold('start') +
66
+ ' [tun|mixed] 启动/切换代理\n' +
67
+ ' ' +
68
+ colors.bold('sub') +
69
+ ' [use|update] 订阅管理\n' +
70
+ ' ' +
71
+ colors.bold('ow') +
72
+ ' [on|off] 覆写配置\n' +
73
+ ' ' +
74
+ colors.bold('ui') +
75
+ ' [zash|dash|yacd] 打开 Web UI\n',
59
76
  );
60
77
  }
61
78
 
62
79
  function printHelp() {
63
80
  console.log(
64
- '\nmihomo-cli v' +
65
- VERSION +
81
+ '\n' +
82
+ colors.cyan(colors.bold('mihomo-cli v' + VERSION)) +
66
83
  '\n' +
67
84
  '\n' +
68
85
  '命令别名: mihomo, mmc, mh\n' +
@@ -70,47 +87,95 @@ function printHelp() {
70
87
  '用法:\n' +
71
88
  ' mihomo <命令> [选项]\n' +
72
89
  '\n' +
73
- '控制:\n' +
74
- ' start [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
75
- ' stop 停止代理\n' +
76
- ' status 查看状态\n' +
90
+ colors.cyan('控制:') +
91
+ '\n' +
92
+ ' ' +
93
+ colors.bold('start') +
94
+ ' [tun|mixed] 启动/切换代理 (默认 mixed)\n' +
95
+ ' ' +
96
+ colors.bold('stop') +
97
+ ' 停止代理\n' +
98
+ ' ' +
99
+ colors.bold('status') +
100
+ ' 查看状态\n' +
101
+ '\n' +
102
+ colors.cyan('界面:') +
103
+ '\n' +
104
+ ' ' +
105
+ colors.bold('ui') +
106
+ ' [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
107
+ ' ' +
108
+ colors.bold('log') +
109
+ ' [-o] 实时日志(-o 打开文件)\n' +
110
+ ' ' +
111
+ colors.bold('logs') +
112
+ ' [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
113
+ '\n' +
114
+ colors.cyan('订阅:') +
115
+ '\n' +
116
+ ' ' +
117
+ colors.bold('subscription') +
118
+ ' 列出所有订阅(别名 sub)\n' +
119
+ ' ' +
120
+ colors.bold('subscription') +
121
+ ' add <url> [name] 添加订阅\n' +
122
+ ' ' +
123
+ colors.bold('subscription') +
124
+ ' update [name] 更新订阅(无参更新所有)\n' +
125
+ ' ' +
126
+ colors.bold('subscription') +
127
+ ' use <name> 切换默认订阅\n' +
128
+ ' ' +
129
+ colors.bold('subscription') +
130
+ ' web [name] 打开订阅页面\n' +
77
131
  '\n' +
78
- '界面:\n' +
79
- ' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
80
- ' log [-o] 实时日志(-o 打开文件)\n' +
81
- ' logs [编号] [-n N] [-o] 日志列表(0=当前,1+=归档)\n' +
132
+ colors.cyan('配置:') +
82
133
  '\n' +
83
- '订阅:\n' +
84
- ' subscription 列出所有订阅(别名 sub)\n' +
85
- ' subscription add <url> [name] 添加订阅\n' +
86
- ' subscription update [name] 更新订阅(无参更新所有)\n' +
87
- ' subscription use <name> 切换默认订阅\n' +
88
- ' subscription web [name] 打开订阅页面\n' +
134
+ ' ' +
135
+ colors.bold('overwrite') +
136
+ ' 查看覆写状态(别名 ow)\n' +
137
+ ' ' +
138
+ colors.bold('overwrite') +
139
+ ' on|off 启用/禁用覆写配置\n' +
140
+ ' ' +
141
+ colors.bold('directory') +
142
+ ' 显示数据目录位置(别名 dir)\n' +
143
+ ' ' +
144
+ colors.bold('directory') +
145
+ ' open [target] 打开目录: root|subs|logs|overwrites|...\n' +
89
146
  '\n' +
90
- '配置:\n' +
91
- ' overwrite 查看覆写状态(别名 ow)\n' +
92
- ' overwrite on|off 启用/禁用覆写配置\n' +
93
- ' directory 显示数据目录位置(别名 dir)\n' +
94
- ' directory open [target] 打开目录: root|subs|logs|overwrites|...\n' +
147
+ colors.cyan('系统:') +
95
148
  '\n' +
96
- '系统:\n' +
97
- ' kernel [镜像|--no-mirror] 更新内核\n' +
98
- ' update 更新 mihomo-cli (npm install -g)\n' +
99
- ' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
100
- ' help, -h 显示帮助\n' +
101
- ' version, -v 显示版本\n' +
149
+ ' ' +
150
+ colors.bold('kernel') +
151
+ ' [镜像|--no-mirror] 更新内核\n' +
152
+ ' ' +
153
+ colors.bold('update') +
154
+ ' 更新 mihomo-cli (npm install -g)\n' +
155
+ ' ' +
156
+ colors.bold('reset') +
157
+ ' [--full] 重置用户数据 (--full 同时删除内核)\n' +
158
+ ' ' +
159
+ colors.bold('help') +
160
+ ', -h 显示帮助\n' +
161
+ ' ' +
162
+ colors.bold('version') +
163
+ ', -v 显示版本\n' +
164
+ '\n' +
165
+ colors.cyan('示例:') +
102
166
  '\n' +
103
- '示例:\n' +
104
167
  ' mihomo start # 启动/重启 Mixed 模式\n' +
105
168
  ' mihomo start tun # 切换到 TUN 透明代理模式\n' +
106
169
  ' mihomo sub add <url> # 添加订阅 (sub 是 subscription 别名)\n' +
107
170
  ' mihomo ui # 打开 Web UI\n' +
108
171
  '\n' +
109
- '模式说明:\n' +
172
+ colors.cyan('模式说明:') +
173
+ '\n' +
110
174
  ' mixed HTTP + SOCKS5 混合端口 (默认)\n' +
111
175
  ' tun 透明代理,全局自动路由,需要 sudo\n' +
112
176
  '\n' +
113
- '数据目录:\n' +
177
+ colors.cyan('数据目录:') +
178
+ '\n' +
114
179
  ' 环境变量 MIHOMO_CLI_DIR 可自定义位置\n' +
115
180
  ' 默认: ' +
116
181
  config.USER_DATA_DIR +
@@ -120,153 +185,66 @@ function printHelp() {
120
185
 
121
186
  function printVersion() {
122
187
  const kv = config.getKernelVersion() || '未安装';
123
- console.log('mihomo-cli v' + VERSION);
124
- console.log('内核: ' + kv);
125
- console.log('数据目录: ' + config.USER_DATA_DIR);
188
+ console.log(colors.cyan(colors.bold('mihomo-cli v' + VERSION)));
189
+ console.log(colors.gray('内核: ') + kv);
190
+ console.log(colors.gray('数据目录: ') + config.USER_DATA_DIR);
126
191
  }
127
192
 
128
193
  function printStatus() {
129
- const status = processMgr.getStatus();
194
+ const status = processManager.getStatus();
130
195
  const info = config.getConfigInfo();
131
- const owEnabled = overwrite.isOverwriteEnabled();
132
- const owFiles = overwrite.listOverwriteFile().files;
133
- const activeSub = getActiveSubscription();
196
+ const overwriteEnabled = overwrite.isOverwriteEnabled();
197
+ const overwriteFiles = overwrite.listOverwriteFile().files;
198
+ const activeSub = subscription.getActiveSubscription();
134
199
 
135
200
  console.log('');
136
201
  let modeLabel = '';
137
202
  if (info && status.running) {
138
- modeLabel = info.tun ? ' (TUN)' : ' (Mixed)';
203
+ modeLabel = colors.cyan(info.tun ? ' (TUN)' : ' (Mixed)');
139
204
  }
140
- console.log('状态: ' + (status.running ? '运行中' : '已停止') + modeLabel);
141
- console.log('内核: ' + (status.kernelVersion || '未安装'));
205
+ const statusText = status.running ? colors.green('运行中') : colors.yellow('已停止');
206
+ console.log(colors.gray('状态: ') + statusText + modeLabel);
207
+ console.log(colors.gray('内核: ') + (status.kernelVersion || '未安装'));
142
208
 
143
209
  if (status.pid) {
144
- console.log('PID: ' + status.pid);
210
+ console.log(colors.gray('PID: ') + status.pid);
145
211
  if (status.processInfo) {
146
- console.log('内存: ' + status.processInfo.memory);
212
+ console.log(colors.gray('内存: ') + status.processInfo.memory);
147
213
  }
148
214
  }
149
215
 
150
216
  if (info) {
151
217
  if (info.mixedPort) {
152
- console.log('端口: ' + info.mixedPort);
218
+ console.log(colors.gray('端口: ') + info.mixedPort);
153
219
  } else {
154
220
  let ports = [];
155
221
  if (info.httpPort) ports.push('HTTP:' + info.httpPort);
156
222
  if (info.socksPort) ports.push('SOCKS:' + info.socksPort);
157
- console.log('端口: ' + (ports.length > 0 ? ports.join(', ') : '未知'));
223
+ console.log(colors.gray('端口: ') + (ports.length > 0 ? ports.join(', ') : '未知'));
158
224
  }
159
225
  }
160
226
 
161
227
  if (activeSub) {
162
- let subLine = '订阅: ' + activeSub.name;
228
+ let subLine = colors.gray('订阅: ') + activeSub.name;
163
229
  if (info) {
164
230
  subLine += ' (' + subscription.formatProxySummary(info) + ')';
165
231
  }
166
232
  console.log(subLine);
167
233
  } else {
168
- console.log('订阅: 未配置');
234
+ console.log(colors.gray('订阅: ') + '未配置');
169
235
  }
170
236
 
171
- if (owEnabled && owFiles.length > 0) {
172
- const names = owFiles.map(f => f.name).join(', ');
173
- console.log('覆写: 已启用 (' + names + ')');
174
- } else if (owEnabled) {
175
- console.log('覆写: 已启用 (无文件)');
237
+ if (overwriteEnabled && overwriteFiles.length > 0) {
238
+ const names = overwriteFiles.map(f => f.name).join(', ');
239
+ console.log(colors.gray('覆写: ') + colors.green('已启用') + ' (' + names + ')');
240
+ } else if (overwriteEnabled) {
241
+ console.log(colors.gray('覆写: ') + colors.green('已启用') + ' (无文件)');
176
242
  } else {
177
- console.log('覆写: 已禁用');
243
+ console.log(colors.gray('覆写: ') + colors.yellow('已禁用'));
178
244
  }
179
245
  console.log('');
180
246
  }
181
247
 
182
- function getActiveSubscription() {
183
- const subs = config.getSubscriptions();
184
- if (subs.length === 0) {
185
- return null;
186
- }
187
- return subs[0];
188
- }
189
-
190
- function findSubscriptionFuzzy(subs, pattern) {
191
- const lowerPattern = pattern.toLowerCase();
192
- let exact = [];
193
- let prefix = [];
194
- let includes = [];
195
-
196
- for (const s of subs) {
197
- const name = s.name.toLowerCase();
198
- if (name === lowerPattern) {
199
- exact.push(s);
200
- } else if (name.startsWith(lowerPattern)) {
201
- prefix.push(s);
202
- } else if (name.includes(lowerPattern)) {
203
- includes.push(s);
204
- }
205
- }
206
-
207
- if (exact.length > 0) return exact;
208
- if (prefix.length > 0) return prefix;
209
- return includes;
210
- }
211
-
212
- function pickSingleSubscription(subs, pattern) {
213
- if (subs.length === 0) {
214
- console.error('错误: 未找到匹配 "' + pattern + '" 的订阅');
215
- process.exit(1);
216
- }
217
- if (subs.length === 1) {
218
- return subs[0];
219
- }
220
- console.error('错误: 匹配到多个订阅,请更精确指定');
221
- console.log('\n匹配的订阅:');
222
- subs.forEach(s => console.log(' ' + s.name));
223
- process.exit(1);
224
- }
225
-
226
-
227
- function openLogFile(logPath, label) {
228
- const displayLabel = label || logPath;
229
- console.log('用系统默认程序打开: ' + displayLabel);
230
- const success = processMgr.openUrl(logPath);
231
- if (!success) {
232
- console.log('请手动打开: ' + logPath);
233
- }
234
- }
235
-
236
- function openDir(dirPath, label) {
237
- const displayLabel = label || dirPath;
238
- console.log('正在打开: ' + displayLabel);
239
- const success = processMgr.openUrl(dirPath);
240
- if (!success) {
241
- console.log('请手动打开: ' + dirPath);
242
- }
243
- }
244
-
245
- function viewLogWithTail(logPath, options) {
246
- const follow = options && options.follow;
247
- const lines = (options && options.lines) || 100;
248
-
249
- console.log('日志: ' + logPath);
250
- if (follow) {
251
- console.log('按 Ctrl+C 退出\n');
252
- } else {
253
- console.log('显示最后 ' + lines + ' 行\n');
254
- }
255
-
256
- const tailArgs = [];
257
- if (follow) tailArgs.push('-f');
258
- tailArgs.push('-n', lines.toString());
259
- tailArgs.push(logPath);
260
-
261
- const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
262
-
263
- tail.on('close', () => process.exit(0));
264
- tail.on('error', e => {
265
- console.error('无法读取日志: ' + e.message);
266
- process.exit(1);
267
- });
268
- }
269
-
270
248
  async function cmdStart(args) {
271
249
  if (!config.hasKernel()) {
272
250
  console.error('错误: 未找到内核,请运行 "mihomo kernel"');
@@ -275,7 +253,7 @@ async function cmdStart(args) {
275
253
 
276
254
  const targetMode = args[1] === 'tun' ? 'tun' : 'mixed';
277
255
 
278
- const sub = getActiveSubscription();
256
+ const sub = subscription.getActiveSubscription();
279
257
  if (!sub) {
280
258
  console.error('错误: 没有订阅,请先添加订阅');
281
259
  process.exit(1);
@@ -283,8 +261,7 @@ async function cmdStart(args) {
283
261
 
284
262
  await subscription.autoUpdateStaleSubscription();
285
263
 
286
- // 每次 start 都先确保完全干净的状态(停止进程 + 清理运行时文件)
287
- const status = processMgr.getStatus();
264
+ const status = processManager.getStatus();
288
265
  const hasProcess = status.running || status.allProcesses.length > 0;
289
266
 
290
267
  if (hasProcess) {
@@ -292,56 +269,55 @@ async function cmdStart(args) {
292
269
  console.log('停止 ' + count + ' 个进程...');
293
270
  }
294
271
 
295
- // 总是调用 stop(即使没进程也会清理 PID 文件和运行时目录)
296
- const stopResult = processMgr.stop(true);
272
+ const stopResult = processManager.stop(true);
297
273
 
298
274
  if (stopResult.remaining && stopResult.remaining.length > 0) {
299
- console.error('部分进程未终止: ' + stopResult.remaining.join(', '));
275
+ console.error(colors.red('部分进程未终止:') + ' ' + stopResult.remaining.join(', '));
300
276
  console.error('请手动运行: sudo pkill -9 mihomo');
301
277
  process.exit(1);
302
278
  }
303
279
 
304
280
  if (hasProcess) {
305
- console.log('已停止\n');
281
+ console.log(colors.green('已停止') + '\n');
306
282
  }
307
283
 
308
- let cfgInfo;
284
+ let configInfo;
309
285
  try {
310
- cfgInfo = subscription.prepareConfigForStart(targetMode, sub.name);
286
+ configInfo = subscription.prepareConfigForStart(targetMode, sub.name);
311
287
  } catch (e) {
312
- console.error('配置错误: ' + e.message);
288
+ console.error(colors.red('配置错误:') + ' ' + e.message);
313
289
  process.exit(1);
314
290
  }
315
291
 
316
292
  const modeLabel = targetMode === 'tun' ? 'TUN' : 'Mixed';
317
- console.log([modeLabel, sub.name, subscription.formatProxySummary(cfgInfo)].join(' · '));
293
+ console.log([colors.cyan(modeLabel), sub.name, subscription.formatProxySummary(configInfo)].join(' · '));
318
294
 
319
295
  try {
320
- const result = await processMgr.start(targetMode);
321
- console.log('已启动 (PID ' + result.pid + ')');
296
+ const result = await processManager.start(targetMode);
297
+ console.log(colors.green('已启动') + ' (PID ' + result.pid + ')');
322
298
  printStatus();
323
299
  } catch (e) {
324
- console.error('启动失败: ' + e.message.split('\n')[0]);
300
+ console.error(colors.red('启动失败:') + ' ' + e.message.split('\n')[0]);
325
301
  process.exit(1);
326
302
  }
327
303
  }
328
304
 
329
305
  async function cmdStop() {
330
- const pids = processMgr.getAllMihomoPids();
306
+ const pids = processManager.getAllMihomoPids();
331
307
  if (pids.length === 0) {
332
- console.log('未在运行');
308
+ console.log(colors.yellow('未在运行'));
333
309
  return;
334
310
  }
335
311
 
336
312
  console.log('停止 ' + pids.length + ' 个进程...');
337
- const result = processMgr.stop(true);
313
+ const result = processManager.stop(true);
338
314
 
339
315
  if (result.remaining && result.remaining.length > 0) {
340
- console.error('部分进程未终止: ' + result.remaining.join(', '));
316
+ console.error(colors.red('部分进程未终止:') + ' ' + result.remaining.join(', '));
341
317
  console.error('请手动运行: sudo pkill -9 mihomo');
342
318
  process.exit(1);
343
319
  }
344
- console.log('已停止');
320
+ console.log(colors.green('已停止'));
345
321
  }
346
322
 
347
323
  function cmdUI(args) {
@@ -357,21 +333,21 @@ function cmdUI(args) {
357
333
  console.log('打开 Web UI: ' + uiName);
358
334
  console.log('地址: ' + url);
359
335
 
360
- const success = processMgr.openUrl(url);
336
+ const success = processManager.openUrl(url);
361
337
  if (!success) {
362
338
  console.log('请手动访问上面的地址');
363
339
  }
364
340
  }
365
341
 
366
342
  function cmdLog(args) {
367
- const logPath = processMgr.getLogPath();
343
+ const logPath = processManager.getLogPath();
368
344
 
369
345
  if (utils.hasFlag(args, '-o', '--open')) {
370
- openLogFile(logPath);
346
+ processManager.openLogFile(logPath);
371
347
  return;
372
348
  }
373
349
 
374
- viewLogWithTail(logPath, { follow: true, lines: 50 });
350
+ processManager.viewLogWithTail(logPath, { follow: true, lines: 50 });
375
351
  }
376
352
 
377
353
  function cmdLogs(args) {
@@ -383,12 +359,11 @@ function cmdLogs(args) {
383
359
  let logPath;
384
360
 
385
361
  if (targetName === 'current' || targetName === '0') {
386
- logPath = processMgr.getLogPath();
362
+ logPath = processManager.getLogPath();
387
363
  } else {
388
- // 纯数字 1+ 表示归档日志的位置(最新=1)
389
364
  const parsedIdx = parseInt(targetName);
390
365
  if (!isNaN(parsedIdx) && parsedIdx > 0 && String(parsedIdx) === targetName) {
391
- const archiveLogs = processMgr.listLogs();
366
+ const archiveLogs = processManager.listLogs();
392
367
  const archive = archiveLogs.archives[parsedIdx - 1];
393
368
  if (!archive) {
394
369
  console.error('错误: 未找到日志 "' + targetName + '"');
@@ -397,7 +372,7 @@ function cmdLogs(args) {
397
372
  }
398
373
  logPath = archive.path;
399
374
  } else {
400
- logPath = processMgr.getLogPathByName(targetName);
375
+ logPath = processManager.getLogPathByName(targetName);
401
376
  }
402
377
  }
403
378
 
@@ -408,15 +383,15 @@ function cmdLogs(args) {
408
383
  }
409
384
 
410
385
  if (openInViewer) {
411
- openLogFile(logPath);
386
+ processManager.openLogFile(logPath);
412
387
  return;
413
388
  }
414
389
 
415
- viewLogWithTail(logPath, { follow: false, lines });
390
+ processManager.viewLogWithTail(logPath, { follow: false, lines });
416
391
  return;
417
392
  }
418
393
 
419
- const logs = processMgr.listLogs();
394
+ const logs = processManager.listLogs();
420
395
  const all = [];
421
396
 
422
397
  if (logs.current) {
@@ -462,55 +437,8 @@ function cmdLogs(args) {
462
437
  console.log('');
463
438
  }
464
439
 
465
- // 解析镜像参数
466
- function parseMirrorArg(args) {
467
- // 返回: { mirror: 镜像URL|null, isOverride: boolean }
468
- // mirror = null 表示禁用镜像
469
- // mirror = undefined 表示使用默认/配置
470
-
471
- if (!args || args.length < 2) {
472
- return { mirror: undefined, isOverride: false };
473
- }
474
-
475
- // 检查 --no-mirror
476
- if (args.includes('--no-mirror') || args.includes('--direct')) {
477
- return { mirror: null, isOverride: true };
478
- }
479
-
480
- // 检查 --mirror <值>
481
- const mirrorIdx = args.indexOf('--mirror');
482
- if (mirrorIdx >= 0 && mirrorIdx + 1 < args.length) {
483
- let mirrorVal = args[mirrorIdx + 1];
484
- return { mirror: normalizeMirrorUrl(mirrorVal), isOverride: true };
485
- }
486
-
487
- // 第一个非 flag 参数作为镜像
488
- for (let i = 1; i < args.length; i++) {
489
- const arg = args[i];
490
- if (!arg.startsWith('-')) {
491
- return { mirror: normalizeMirrorUrl(arg), isOverride: true };
492
- }
493
- }
494
-
495
- return { mirror: undefined, isOverride: false };
496
- }
497
-
498
- function normalizeMirrorUrl(val) {
499
- if (!val) return null;
500
- if (val === 'direct' || val === 'no' || val === 'none') return null;
501
-
502
- let url = val;
503
- if (!url.startsWith('http')) {
504
- url = 'https://' + url;
505
- }
506
- if (!url.endsWith('/')) {
507
- url += '/';
508
- }
509
- return url;
510
- }
511
-
512
440
  async function cmdKernel(args) {
513
- const mirrorInfo = parseMirrorArg(args);
441
+ const mirrorInfo = utils.parseMirrorArg(args);
514
442
  const effectiveMirror = mirrorInfo.isOverride ? mirrorInfo.mirror : config.getGitHubMirror();
515
443
  const isDefault = !mirrorInfo.isOverride && effectiveMirror === config.DEFAULT_GITHUB_MIRROR;
516
444
 
@@ -553,7 +481,7 @@ async function cmdKernel(args) {
553
481
  console.log('\n正在下载...');
554
482
  const result = await kernel.downloadKernel(msg => {
555
483
  console.log(msg);
556
- }, mirrorInfo.mirror); // 传递镜像参数(undefined = 用配置,null = 禁用)
484
+ }, mirrorInfo.mirror);
557
485
  console.log('已更新到 ' + result.version);
558
486
  } catch (e) {
559
487
  console.error('更新失败: ' + e.message);
@@ -575,16 +503,16 @@ async function printSubscriptionList() {
575
503
  console.log('');
576
504
  return;
577
505
  }
578
- console.log('订阅列表:');
506
+ console.log(colors.cyan('订阅列表:'));
579
507
  subs.forEach((s, i) => {
580
508
  const time = utils.formatDate(s.updated_at);
581
- const defaultMark = i === 0 ? ' [默认]' : '';
509
+ const defaultMark = i === 0 ? colors.green(' [默认]') : '';
582
510
  const interval = s.update_interval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
583
511
  console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
584
- console.log(' 更新: ' + time + ' (间隔: ' + interval + 'h)');
512
+ console.log(' ' + colors.gray('更新:') + ' ' + time + ' (间隔: ' + interval + 'h)');
585
513
 
586
514
  if (s.username) {
587
- console.log(' 用户: ' + s.username);
515
+ console.log(' ' + colors.gray('用户:') + ' ' + s.username);
588
516
  }
589
517
  if (s.download !== undefined || s.total !== undefined) {
590
518
  const used = (s.upload || 0) + (s.download || 0);
@@ -595,13 +523,13 @@ async function printSubscriptionList() {
595
523
  const percent = Math.min((used / s.total) * 100, 100);
596
524
  percentStr = ' (' + percent.toFixed(1) + '%)';
597
525
  }
598
- console.log(' 流量: ' + usedStr + ' / ' + totalStr + percentStr);
526
+ console.log(' ' + colors.gray('流量:') + ' ' + usedStr + ' / ' + totalStr + percentStr);
599
527
  }
600
528
  if (s.expire !== undefined) {
601
- console.log(' 到期: ' + utils.formatTimestamp(s.expire));
529
+ console.log(' ' + colors.gray('到期:') + ' ' + utils.formatTimestamp(s.expire));
602
530
  }
603
531
  if (s.web_page_url) {
604
- console.log(' 页面: ' + s.web_page_url);
532
+ console.log(' ' + colors.gray('页面:') + ' ' + s.web_page_url);
605
533
  }
606
534
  });
607
535
  console.log('');
@@ -659,9 +587,9 @@ async function cmdSubscription(args) {
659
587
  results.forEach(r => {
660
588
  if (r.success) {
661
589
  ok++;
662
- console.log('✓ ' + r.name + ': 已更新 (' + subscription.formatProxySummary(r) + ')');
590
+ console.log(colors.green('✓') + ' ' + r.name + ': ' + colors.green('已更新') + ' (' + subscription.formatProxySummary(r) + ')');
663
591
  } else {
664
- console.log('✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
592
+ console.log(colors.red('✗') + ' ' + r.name + ': ' + colors.red('失败') + ' (' + r.error.split('\n')[0] + ')');
665
593
  }
666
594
  });
667
595
  if (ok === 0) process.exit(1);
@@ -670,8 +598,8 @@ async function cmdSubscription(args) {
670
598
  return;
671
599
  }
672
600
 
673
- const matches = findSubscriptionFuzzy(subs, name);
674
- const target = pickSingleSubscription(matches, name);
601
+ const matches = subscription.findSubscriptionFuzzy(subs, name);
602
+ const target = subscription.pickSingleSubscription(matches, name);
675
603
 
676
604
  console.log('更新订阅: ' + target.name);
677
605
  try {
@@ -699,11 +627,10 @@ async function cmdSubscription(args) {
699
627
  process.exit(1);
700
628
  }
701
629
 
702
- const matches = findSubscriptionFuzzy(subs, name);
703
- const target = pickSingleSubscription(matches, name);
630
+ const matches = subscription.findSubscriptionFuzzy(subs, name);
631
+ const target = subscription.pickSingleSubscription(matches, name);
704
632
 
705
- // 检查是否已是当前默认订阅
706
- const currentDefault = getActiveSubscription();
633
+ const currentDefault = subscription.getActiveSubscription();
707
634
  const isAlreadyDefault = currentDefault && currentDefault.name === target.name;
708
635
 
709
636
  if (isAlreadyDefault) {
@@ -713,10 +640,9 @@ async function cmdSubscription(args) {
713
640
  return;
714
641
  }
715
642
 
716
- // 检查当前运行状态和模式
717
- const status = processMgr.getStatus();
718
- const cfgInfo = config.getConfigInfo();
719
- const currentMode = cfgInfo && cfgInfo.tun ? 'tun' : 'mixed';
643
+ const status = processManager.getStatus();
644
+ const configInfo = config.getConfigInfo();
645
+ const currentMode = configInfo && configInfo.tun ? 'tun' : 'mixed';
720
646
 
721
647
  const success = config.setDefaultSubscription(target.name);
722
648
  if (success) {
@@ -726,7 +652,6 @@ async function cmdSubscription(args) {
726
652
  process.exit(1);
727
653
  }
728
654
 
729
- // 如果正在运行,自动重启
730
655
  if (status.running) {
731
656
  console.log('');
732
657
  await cmdStart(['start', currentMode]);
@@ -749,8 +674,8 @@ async function cmdSubscription(args) {
749
674
 
750
675
  let target;
751
676
  if (name) {
752
- const matches = findSubscriptionFuzzy(subs, name);
753
- target = pickSingleSubscription(matches, name);
677
+ const matches = subscription.findSubscriptionFuzzy(subs, name);
678
+ target = subscription.pickSingleSubscription(matches, name);
754
679
  } else {
755
680
  target = subs[0];
756
681
  }
@@ -759,8 +684,7 @@ async function cmdSubscription(args) {
759
684
  if (!webPageUrl) {
760
685
  console.log('订阅信息中缺少页面地址,正在更新订阅...');
761
686
  try {
762
- const info = await subscription.downloadSubscription(target.url, target.name);
763
- // 重新读取缓存获取 web_page_url
687
+ await subscription.downloadSubscription(target.url, target.name);
764
688
  const cache = config.readSubscriptionsCache();
765
689
  if (cache[target.name] && cache[target.name].web_page_url) {
766
690
  webPageUrl = cache[target.name].web_page_url;
@@ -775,7 +699,7 @@ async function cmdSubscription(args) {
775
699
  }
776
700
 
777
701
  console.log('打开订阅页面: ' + webPageUrl);
778
- const opened = processMgr.openUrl(webPageUrl);
702
+ const opened = processManager.openUrl(webPageUrl);
779
703
  if (!opened) {
780
704
  console.log('请手动访问上面的地址');
781
705
  }
@@ -787,38 +711,57 @@ async function cmdSubscription(args) {
787
711
  process.exit(1);
788
712
  }
789
713
 
790
- function cmdUpdate() {
791
- console.log('更新 mihomo-cli...');
714
+ async function cmdUpdate() {
715
+ console.log('当前版本: ' + colors.cyan(VERSION));
716
+ console.log('');
717
+ console.log('正在更新 mihomo-cli...');
792
718
  console.log('');
793
719
 
794
- const npm = spawn('npm', ['install', '-g', 'mihomo-cli'], { stdio: 'inherit' });
720
+ await new Promise(resolve => {
721
+ const npm = spawn('npm', ['install', '-g', 'mihomo-cli'], { stdio: 'inherit' });
795
722
 
796
- npm.on('close', code => {
797
- if (code === 0) {
798
- console.log('');
799
- console.log('更新完成');
800
- } else {
801
- process.exit(code);
802
- }
803
- });
723
+ npm.on('close', code => {
724
+ if (code === 0) {
725
+ resolve();
726
+ } else {
727
+ process.exit(code);
728
+ }
729
+ });
804
730
 
805
- npm.on('error', e => {
806
- console.error('执行失败: ' + e.message);
807
- process.exit(1);
731
+ npm.on('error', e => {
732
+ console.error('执行失败: ' + e.message);
733
+ process.exit(1);
734
+ });
808
735
  });
736
+
737
+ try {
738
+ const { stdout } = await execAsync('npm list -g mihomo-cli --json --depth=0');
739
+ const result = JSON.parse(stdout);
740
+ const newVersion = result.dependencies?.['mihomo-cli']?.version;
741
+
742
+ console.log('');
743
+ if (newVersion) {
744
+ console.log('更新完成,最新版本: ' + colors.green(newVersion));
745
+ } else {
746
+ console.log('更新完成');
747
+ }
748
+ } catch {
749
+ console.log('');
750
+ console.log('更新完成');
751
+ }
809
752
  }
810
753
 
811
754
  async function cmdReset(args) {
812
755
  const fullReset = args && (args.includes('--full') || args.includes('-f'));
813
756
  const skipConfirm = args && (args.includes('--yes') || args.includes('-y'));
814
757
 
815
- const pids = processMgr.getAllMihomoPids();
758
+ const pids = processManager.getAllMihomoPids();
816
759
  if (pids.length > 0) {
817
760
  console.log('停止 ' + pids.length + ' 个进程...');
818
- processMgr.cleanupAll(true);
819
- for (let i = 0; i < 50; i++) {
820
- if (processMgr.getAllMihomoPids().length === 0) break;
821
- await new Promise(r => setTimeout(r, 100));
761
+ processManager.cleanupAll(true);
762
+ for (let i = 0; i < processManager.PROCESS_WAIT_ATTEMPTS; i++) {
763
+ if (processManager.getAllMihomoPids().length === 0) break;
764
+ await new Promise(r => setTimeout(r, processManager.PROCESS_WAIT_INTERVAL));
822
765
  }
823
766
  }
824
767
 
@@ -826,7 +769,6 @@ async function cmdReset(args) {
826
769
  console.log(mode);
827
770
 
828
771
  if (!skipConfirm) {
829
- const readline = require('readline');
830
772
  const rl = readline.createInterface({
831
773
  input: process.stdin,
832
774
  output: process.stdout,
@@ -851,8 +793,9 @@ async function cmdReset(args) {
851
793
 
852
794
  function printOverwriteList() {
853
795
  const info = overwrite.listOverwriteFile();
854
- console.log('状态: ' + (info.enabled ? '已启用' : '已禁用'));
855
- console.log('目录: ' + info.dir);
796
+ const statusText = info.enabled ? colors.green('已启用') : colors.yellow('已禁用');
797
+ console.log(colors.gray('状态:') + ' ' + statusText);
798
+ console.log(colors.gray('目录:') + ' ' + info.dir);
856
799
  console.log('');
857
800
  if (info.files.length === 0) {
858
801
  console.log('暂无覆写文件');
@@ -860,13 +803,13 @@ function printOverwriteList() {
860
803
  console.log('用法示例: 创建文件 ' + path.join(info.dir, '01-custom.yaml'));
861
804
  console.log('');
862
805
  } else {
863
- console.log('覆写文件 (' + info.files.length + ' 个,按顺序加载):');
806
+ console.log(colors.cyan('覆写文件') + ' (' + info.files.length + ' 个,按顺序加载):');
864
807
  console.log('');
865
808
  info.files.forEach((f, i) => {
866
809
  const num = i < 10 ? ' ' + i : '' + i;
867
810
  console.log(' ' + num + '. ' + f.name);
868
811
  if (f.keys.length > 0) {
869
- console.log(' 字段: ' + f.keys.join(', '));
812
+ console.log(' ' + colors.gray('字段:') + ' ' + f.keys.join(', '));
870
813
  }
871
814
  });
872
815
  console.log('');
@@ -879,13 +822,11 @@ function printOverwriteList() {
879
822
  async function cmdOverwrite(args) {
880
823
  const action = args && args[1];
881
824
 
882
- // 检查当前运行状态和模式
883
- const status = processMgr.getStatus();
884
- const cfgInfo = config.getConfigInfo();
885
- const currentMode = cfgInfo && cfgInfo.tun ? 'tun' : 'mixed';
825
+ const status = processManager.getStatus();
826
+ const configInfo = config.getConfigInfo();
827
+ const currentMode = configInfo && configInfo.tun ? 'tun' : 'mixed';
886
828
 
887
829
  if (action === 'on' || action === 'enable') {
888
- // 如果已经启用,提示后直接返回
889
830
  if (overwrite.isOverwriteEnabled()) {
890
831
  console.log('覆写配置已是启用状态');
891
832
  console.log('');
@@ -896,7 +837,6 @@ async function cmdOverwrite(args) {
896
837
  overwrite.setOverwriteEnabled(true);
897
838
  console.log('已启用覆写配置');
898
839
 
899
- // 如果正在运行,自动重启
900
840
  if (status.running) {
901
841
  console.log('');
902
842
  await cmdStart(['start', currentMode]);
@@ -909,7 +849,6 @@ async function cmdOverwrite(args) {
909
849
  }
910
850
 
911
851
  if (action === 'off' || action === 'disable') {
912
- // 如果已经禁用,提示后直接返回
913
852
  if (!overwrite.isOverwriteEnabled()) {
914
853
  console.log('覆写配置已是禁用状态');
915
854
  console.log('');
@@ -920,7 +859,6 @@ async function cmdOverwrite(args) {
920
859
  overwrite.setOverwriteEnabled(false);
921
860
  console.log('已禁用覆写配置');
922
861
 
923
- // 如果正在运行,自动重启
924
862
  if (status.running) {
925
863
  console.log('');
926
864
  await cmdStart(['start', currentMode]);
@@ -932,23 +870,10 @@ async function cmdOverwrite(args) {
932
870
  return;
933
871
  }
934
872
 
935
- // 无参数、list、ls 都显示文件列表
936
873
  console.log('');
937
874
  printOverwriteList();
938
875
  }
939
876
 
940
- // 目录目标映射(精确匹配)
941
- const DIRECTORY_TARGETS = {
942
- root: { path: null, label: '根目录' },
943
- subs: { path: config.DIRS.subscriptions, label: '订阅目录' },
944
- logs: { path: config.DIRS.logs, label: '日志目录' },
945
- data: { path: config.DIRS.data, label: 'mihomo 数据目录' },
946
- runtime: { path: config.DIRS.runtime, label: '运行时目录' },
947
- overwrites: { path: config.DIRS.overwrites, label: '覆写目录' },
948
- settings: { path: config.PATHS.settingsFile, label: '设置文件' },
949
- kernel: { path: config.DIRS.core, label: '内核目录' },
950
- };
951
-
952
877
  function cmdDirectory(args) {
953
878
  const action = args && args[1];
954
879
 
@@ -956,14 +881,23 @@ function cmdDirectory(args) {
956
881
  const target = args[2];
957
882
 
958
883
  if (!target || target === 'root') {
959
- openDir(config.USER_DATA_DIR, '根目录');
884
+ const displayLabel = '根目录';
885
+ console.log('正在打开: ' + displayLabel);
886
+ const success = processManager.openUrl(config.USER_DATA_DIR);
887
+ if (!success) {
888
+ console.log('请手动打开: ' + config.USER_DATA_DIR);
889
+ }
960
890
  return;
961
891
  }
962
892
 
963
- const targetInfo = DIRECTORY_TARGETS[target.toLowerCase()];
893
+ const targetInfo = config.DIRECTORY_TARGETS[target.toLowerCase()];
964
894
  if (targetInfo) {
965
- const path = targetInfo.path || config.USER_DATA_DIR;
966
- openDir(path, targetInfo.label);
895
+ const targetPath = targetInfo.path || config.USER_DATA_DIR;
896
+ console.log('正在打开: ' + targetInfo.label);
897
+ const success = processManager.openUrl(targetPath);
898
+ if (!success) {
899
+ console.log('请手动打开: ' + targetPath);
900
+ }
967
901
  return;
968
902
  }
969
903
 
@@ -982,7 +916,6 @@ function cmdDirectory(args) {
982
916
  process.exit(1);
983
917
  }
984
918
 
985
- // 无参数或未知参数:显示目录列表
986
919
  console.log('');
987
920
  console.log('数据目录位置:');
988
921
  console.log(' 根目录: ' + config.USER_DATA_DIR);
@@ -1059,7 +992,7 @@ async function main() {
1059
992
  case 'upd':
1060
993
  case 'update':
1061
994
  case 'upgrade':
1062
- cmdUpdate();
995
+ await cmdUpdate();
1063
996
  break;
1064
997
  case 'sub':
1065
998
  case 'subscription':