mihomo-cli 1.2.4 → 1.3.0

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