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/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "1.2.4",
3
+ "version": "1.3.0",
4
4
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
5
- "main": "index.js",
6
5
  "bin": {
7
6
  "mihomo-cli": "index.js",
8
7
  "mihomo": "index.js",
@@ -15,7 +14,10 @@
15
14
  "CHANGELOG.md"
16
15
  ],
17
16
  "scripts": {
18
- "start": "node index.js"
17
+ "format": "prettier --write \"**/*.{js,json,md}\"",
18
+ "lint": "eslint .",
19
+ "lint:fix": "eslint . --fix",
20
+ "prepare": "husky"
19
21
  },
20
22
  "keywords": [
21
23
  "mihomo",
@@ -26,21 +28,26 @@
26
28
  "cli",
27
29
  "macos"
28
30
  ],
29
- "author": "",
31
+ "author": "Aex",
30
32
  "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/adaex/mihomo-cli.git"
36
+ },
31
37
  "engines": {
32
38
  "node": ">=18.0.0"
33
39
  },
34
- "os": [
35
- "darwin"
36
- ],
37
- "cpu": [
38
- "x64",
39
- "arm64"
40
- ],
41
40
  "dependencies": {
42
41
  "axios": "^1.6.0",
43
42
  "compare-versions": "^6.1.0",
44
43
  "js-yaml": "^4.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "@eslint/js": "^10.0.1",
47
+ "eslint": "^10.2.0",
48
+ "globals": "^17.4.0",
49
+ "husky": "^9.1.7",
50
+ "lint-staged": "^16.4.0",
51
+ "prettier": "^3.0.0"
45
52
  }
46
53
  }
package/src/config.js CHANGED
@@ -1,9 +1,15 @@
1
+ // 内置模块
1
2
  const path = require('path');
2
3
  const fs = require('fs');
3
4
  const os = require('os');
4
- const yaml = require('js-yaml');
5
5
  const { execSync } = require('child_process');
6
6
 
7
+ // 第三方模块
8
+ const yaml = require('js-yaml');
9
+
10
+ // 本地模块
11
+ // (无额外本地模块,overwrite.js 在 buildConfig 中延迟加载以避免循环依赖)
12
+
7
13
  const IS_PKG = typeof process.pkg !== 'undefined';
8
14
 
9
15
  let PROJECT_ROOT;
@@ -75,23 +81,23 @@ function maskUrl(url) {
75
81
  }
76
82
  }
77
83
 
78
- let _settingsCache = null;
84
+ let settingsCache = null;
79
85
 
80
86
  function readSettings() {
81
- if (_settingsCache !== null) return _settingsCache;
87
+ if (settingsCache !== null) return settingsCache;
82
88
  ensureDirs();
83
89
  if (fs.existsSync(PATHS.settingsFile)) {
84
90
  try {
85
91
  const content = fs.readFileSync(PATHS.settingsFile, 'utf8');
86
- _settingsCache = JSON.parse(content);
87
- return _settingsCache;
88
- } catch (e) {
89
- _settingsCache = {};
90
- return _settingsCache;
92
+ settingsCache = JSON.parse(content);
93
+ return settingsCache;
94
+ } catch (_e) {
95
+ settingsCache = {};
96
+ return settingsCache;
91
97
  }
92
98
  }
93
- _settingsCache = {};
94
- return _settingsCache;
99
+ settingsCache = {};
100
+ return settingsCache;
95
101
  }
96
102
 
97
103
  function writeSettings(settings) {
@@ -103,7 +109,7 @@ function writeSettings(settings) {
103
109
  if (settings[key] === undefined) delete merged[key];
104
110
  }
105
111
  fs.writeFileSync(PATHS.settingsFile, JSON.stringify(merged, null, 2), { mode: 0o600 });
106
- _settingsCache = merged;
112
+ settingsCache = merged;
107
113
  return merged;
108
114
  }
109
115
 
@@ -150,28 +156,28 @@ function setGitHubMirror(mirror) {
150
156
  }
151
157
 
152
158
  // 订阅缓存读写(动态数据:流量、用户名、更新时间等)
153
- function readSubscriptionsCache() {
159
+ function readSubscriptionCache() {
154
160
  ensureDirs();
155
161
  if (fs.existsSync(PATHS.subscriptionsCacheFile)) {
156
162
  try {
157
163
  const content = fs.readFileSync(PATHS.subscriptionsCacheFile, 'utf8');
158
164
  return JSON.parse(content);
159
- } catch (e) {
165
+ } catch (_e) {
160
166
  return {};
161
167
  }
162
168
  }
163
169
  return {};
164
170
  }
165
171
 
166
- function writeSubscriptionsCache(cache) {
172
+ function writeSubscriptionCache(cache) {
167
173
  ensureDirs();
168
174
  fs.writeFileSync(PATHS.subscriptionsCacheFile, JSON.stringify(cache, null, 2), { mode: 0o600 });
169
175
  }
170
176
 
171
177
  function saveSubscriptionCache(subName, data) {
172
- const cache = readSubscriptionsCache();
178
+ const cache = readSubscriptionCache();
173
179
  cache[subName] = { ...cache[subName], ...data };
174
- writeSubscriptionsCache(cache);
180
+ writeSubscriptionCache(cache);
175
181
  }
176
182
 
177
183
  function getSubscriptions() {
@@ -182,7 +188,7 @@ function getSubscriptions() {
182
188
  // 获取合并了缓存数据的订阅列表
183
189
  function getSubscriptionsWithCache() {
184
190
  const subs = getSubscriptions();
185
- const cache = readSubscriptionsCache();
191
+ const cache = readSubscriptionCache();
186
192
  return subs.map(s => ({
187
193
  ...s,
188
194
  ...(cache[s.name] || {}),
@@ -218,18 +224,18 @@ function setDefaultSubscription(name) {
218
224
  return true;
219
225
  }
220
226
 
221
- function getSubRawConfigPath(subName) {
227
+ function getSubscriptionRawConfigPath(subName) {
222
228
  return path.join(DIRS.subscriptions, subName + '.yaml');
223
229
  }
224
230
 
225
- function saveSubRawConfig(subName, content) {
231
+ function saveSubscriptionRawConfig(subName, content) {
226
232
  ensureDirs();
227
- const filePath = getSubRawConfigPath(subName);
233
+ const filePath = getSubscriptionRawConfigPath(subName);
228
234
  fs.writeFileSync(filePath, content, { mode: 0o600 });
229
235
  }
230
236
 
231
- function readSubRawConfig(subName) {
232
- const filePath = getSubRawConfigPath(subName);
237
+ function readSubscriptionRawConfig(subName) {
238
+ const filePath = getSubscriptionRawConfigPath(subName);
233
239
  if (!fs.existsSync(filePath)) {
234
240
  return null;
235
241
  }
@@ -240,28 +246,28 @@ function hasKernel() {
240
246
  return fs.existsSync(PATHS.mihomoBinary);
241
247
  }
242
248
 
243
- let _kernelVersionCache = undefined;
249
+ let kernelVersionCache = undefined;
244
250
 
245
251
  function getKernelVersion() {
246
252
  if (!hasKernel()) {
247
- _kernelVersionCache = undefined;
253
+ kernelVersionCache = undefined;
248
254
  return null;
249
255
  }
250
- if (_kernelVersionCache !== undefined) return _kernelVersionCache;
256
+ if (kernelVersionCache !== undefined) return kernelVersionCache;
251
257
  try {
252
258
  const output = execSync('"' + PATHS.mihomoBinary + '" -v 2>&1 || true', {
253
259
  encoding: 'utf8',
254
260
  }).trim();
255
261
  if (output) {
256
262
  const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
257
- _kernelVersionCache = match ? match[0] : output;
258
- return _kernelVersionCache;
263
+ kernelVersionCache = match ? match[0] : output;
264
+ return kernelVersionCache;
259
265
  }
260
- _kernelVersionCache = 'unknown';
261
- return _kernelVersionCache;
262
- } catch (e) {
263
- _kernelVersionCache = 'unknown';
264
- return _kernelVersionCache;
266
+ kernelVersionCache = 'unknown';
267
+ return kernelVersionCache;
268
+ } catch (_e) {
269
+ kernelVersionCache = 'unknown';
270
+ return kernelVersionCache;
265
271
  }
266
272
  }
267
273
 
@@ -296,10 +302,10 @@ function parseYamlOrJson(content, errorMsg) {
296
302
  try {
297
303
  const result = yaml.load(content);
298
304
  if (result !== undefined) return result;
299
- } catch (e) {}
305
+ } catch (_e) {}
300
306
  try {
301
307
  return JSON.parse(content);
302
- } catch (e2) {
308
+ } catch (_e2) {
303
309
  throw new Error((errorMsg || '内容') + '格式错误,无法解析为 YAML 或 JSON');
304
310
  }
305
311
  }
@@ -369,7 +375,7 @@ function getConfigInfo() {
369
375
  socksPort: cfg['socks-port'] || null,
370
376
  tun: cfg.tun ? cfg.tun.enable : false,
371
377
  };
372
- } catch (e) {
378
+ } catch (_e) {
373
379
  return null;
374
380
  }
375
381
  }
@@ -401,30 +407,43 @@ function resetUserData(options) {
401
407
  }
402
408
 
403
409
  ensureDirs();
404
- _settingsCache = null;
410
+ settingsCache = null;
405
411
  return removedCount;
406
412
  }
407
413
 
414
+ // 目录目标映射(从 index.js 移入,精确匹配)
415
+ const DIRECTORY_TARGETS = {
416
+ root: { path: null, label: '根目录' },
417
+ subs: { path: DIRS.subscriptions, label: '订阅目录' },
418
+ logs: { path: DIRS.logs, label: '日志目录' },
419
+ data: { path: DIRS.data, label: 'mihomo 数据目录' },
420
+ runtime: { path: DIRS.runtime, label: '运行时目录' },
421
+ overwrites: { path: DIRS.overwrites, label: '覆写目录' },
422
+ settings: { path: PATHS.settingsFile, label: '设置文件' },
423
+ kernel: { path: DIRS.core, label: '内核目录' },
424
+ };
425
+
408
426
  module.exports = {
409
427
  PATHS,
410
428
  DIRS,
411
429
  USER_DATA_DIR,
430
+ DIRECTORY_TARGETS,
412
431
  ensureDirs,
413
432
  readSettings,
414
433
  writeSettings,
415
- readSubscriptionsCache,
434
+ readSubscriptionCache,
416
435
  saveSubscriptionCache,
417
436
  maskUrl,
418
437
  getSubscriptions,
419
438
  getSubscriptionsWithCache,
420
439
  addSubscription,
421
440
  setDefaultSubscription,
422
- saveSubRawConfig,
423
- readSubRawConfig,
441
+ saveSubscriptionRawConfig,
442
+ readSubscriptionRawConfig,
424
443
  hasKernel,
425
444
  getKernelVersion,
426
445
  clearKernelVersionCache: () => {
427
- _kernelVersionCache = undefined;
446
+ kernelVersionCache = undefined;
428
447
  },
429
448
  getGitHubMirror,
430
449
  setGitHubMirror,
package/src/kernel.js CHANGED
@@ -1,11 +1,26 @@
1
- const axios = require('axios');
1
+ // 内置模块
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { execSync } = require('child_process');
5
+
6
+ // 第三方模块
5
7
  const { compareVersions } = require('compare-versions');
8
+
9
+ // 本地模块
6
10
  const config = require('./config');
11
+ const utils = require('./utils');
7
12
 
13
+ // 常量定义
8
14
  const GITHUB_REPO = 'MetaCubeX/mihomo';
15
+ const KERNEL_HTTP_TIMEOUT = 120000;
16
+ const KERNEL_MAX_CONTENT_LENGTH = 200 * 1024 * 1024;
17
+ const KERNEL_DOWNLOAD_TIMEOUT = 180000;
18
+
19
+ // 内核专用 HTTP 客户端(超时和容量较大,适合下载大文件)
20
+ const HTTP_CLIENT = utils.createHttpClient({
21
+ timeout: KERNEL_HTTP_TIMEOUT,
22
+ maxContentLength: KERNEL_MAX_CONTENT_LENGTH,
23
+ });
9
24
 
10
25
  function withMirror(url, overrideMirror) {
11
26
  const mirror = overrideMirror !== undefined ? overrideMirror : config.getGitHubMirror();
@@ -15,12 +30,6 @@ function withMirror(url, overrideMirror) {
15
30
  return url;
16
31
  }
17
32
 
18
- const HTTP_CLIENT = axios.create({
19
- timeout: 120000,
20
- headers: { 'User-Agent': 'mihomo-cli' },
21
- maxContentLength: 200 * 1024 * 1024,
22
- });
23
-
24
33
  function getArch() {
25
34
  const arch = process.arch;
26
35
  if (arch === 'arm64') return 'arm64';
@@ -94,7 +103,7 @@ async function checkUpdate() {
94
103
  } else {
95
104
  try {
96
105
  needsUpdate = compareVersions(latestVersion.replace(/^v/, ''), currentVersion.replace(/^v/, '')) > 0;
97
- } catch (e) {
106
+ } catch (_e) {
98
107
  needsUpdate = latestVersion !== currentVersion;
99
108
  }
100
109
  }
@@ -162,7 +171,7 @@ async function downloadKernel(progressCallback, mirror) {
162
171
  method: 'get',
163
172
  url: downloadUrl,
164
173
  responseType: 'stream',
165
- timeout: 180000,
174
+ timeout: KERNEL_DOWNLOAD_TIMEOUT,
166
175
  });
167
176
 
168
177
  const writer = fs.createWriteStream(tempPath);
@@ -225,9 +234,8 @@ async function downloadKernel(progressCallback, mirror) {
225
234
 
226
235
  try {
227
236
  fs.unlinkSync(tempPath);
228
- } catch (e) {}
237
+ } catch (_e) {}
229
238
 
230
- // 内核已更新,清除版本缓存
231
239
  config.clearKernelVersionCache();
232
240
 
233
241
  return {
@@ -237,6 +245,10 @@ async function downloadKernel(progressCallback, mirror) {
237
245
  }
238
246
 
239
247
  module.exports = {
248
+ GITHUB_REPO,
249
+ KERNEL_HTTP_TIMEOUT,
250
+ KERNEL_MAX_CONTENT_LENGTH,
251
+ KERNEL_DOWNLOAD_TIMEOUT,
240
252
  checkUpdate,
241
253
  downloadKernel,
242
254
  };
package/src/overwrite.js CHANGED
@@ -1,6 +1,11 @@
1
+ // 内置模块
1
2
  const fs = require('fs');
2
3
  const path = require('path');
4
+
5
+ // 第三方模块
3
6
  const yaml = require('js-yaml');
7
+
8
+ // 本地模块
4
9
  const config = require('./config');
5
10
 
6
11
  /**
@@ -178,7 +183,8 @@ function loadOverwriteFile() {
178
183
  return [];
179
184
  }
180
185
 
181
- const files = fs.readdirSync(dir)
186
+ const files = fs
187
+ .readdirSync(dir)
182
188
  .filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))
183
189
  .sort();
184
190
 
package/src/process.js CHANGED
@@ -1,9 +1,25 @@
1
+ // 内置模块
1
2
  const fs = require('fs');
2
3
  const path = require('path');
3
4
  const { spawn, execSync } = require('child_process');
5
+
6
+ // 第三方模块
7
+ // (无第三方模块依赖)
8
+
9
+ // 本地模块
4
10
  const config = require('./config');
5
11
  const utils = require('./utils');
6
12
 
13
+ // 进程等待常量
14
+ const PROCESS_WAIT_ATTEMPTS = 50;
15
+ const PROCESS_WAIT_INTERVAL = 100; // ms
16
+ const STARTUP_WAIT_MS = 800; // ms
17
+ const SUDO_TIMEOUT_MS = 60000; // ms
18
+ const TUN_MODE_POST_WAIT_MS = 500; // ms
19
+
20
+ // 日志清理常量
21
+ const DEFAULT_LOG_RETENTION_DAYS = 7;
22
+
7
23
  function clearRuntime() {
8
24
  if (fs.existsSync(config.DIRS.runtime)) {
9
25
  config.rmrf(config.DIRS.runtime);
@@ -18,7 +34,7 @@ function getPid() {
18
34
  try {
19
35
  const pid = parseInt(fs.readFileSync(config.PATHS.pidFile, 'utf8').trim());
20
36
  return pid > 0 ? pid : null;
21
- } catch (e) {
37
+ } catch (_e) {
22
38
  return null;
23
39
  }
24
40
  }
@@ -52,7 +68,7 @@ function isPidFileOwnedByRoot() {
52
68
  try {
53
69
  const stat = fs.statSync(config.PATHS.pidFile);
54
70
  return stat.uid === 0;
55
- } catch (e) {
71
+ } catch (_e) {
56
72
  return false;
57
73
  }
58
74
  }
@@ -86,13 +102,13 @@ function clearPid() {
86
102
  stdio: 'inherit',
87
103
  timeout: 10000,
88
104
  });
89
- } catch (e) {
105
+ } catch (_e) {
90
106
  // 忽略失败,后续操作可能会检测到问题
91
107
  }
92
108
  } else {
93
109
  try {
94
110
  fs.unlinkSync(config.PATHS.pidFile);
95
- } catch (e) {
111
+ } catch (_e) {
96
112
  // ignore
97
113
  }
98
114
  }
@@ -108,11 +124,11 @@ function killProcess(pid, needsSudo) {
108
124
  timeout: 10000,
109
125
  });
110
126
  return true;
111
- } catch (e) {
127
+ } catch (_e) {
112
128
  try {
113
129
  process.kill(pid, 'SIGKILL');
114
130
  return true;
115
- } catch (e2) {
131
+ } catch (_e2) {
116
132
  return false;
117
133
  }
118
134
  }
@@ -120,7 +136,7 @@ function killProcess(pid, needsSudo) {
120
136
  process.kill(pid, 'SIGKILL');
121
137
  return true;
122
138
  }
123
- } catch (e) {
139
+ } catch (_e) {
124
140
  return false;
125
141
  }
126
142
  }
@@ -158,9 +174,7 @@ function cleanupAll(forceSudo) {
158
174
  }
159
175
 
160
176
  const hasRootProcess = pids.some(p => utils.isProcessRoot(p));
161
- const hasRootPidFile = isPidFileOwnedByRoot();
162
177
  const needsSudo = hasRootProcess;
163
- const allowSudo = forceSudo || hasRootProcess || hasRootPidFile;
164
178
 
165
179
  let killedCount = 0;
166
180
  let failedPids = [];
@@ -187,9 +201,10 @@ function cleanupAll(forceSudo) {
187
201
  }
188
202
  }
189
203
 
190
- for (let i = 0; i < 50; i++) {
204
+ // 等待进程终止
205
+ for (let i = 0; i < PROCESS_WAIT_ATTEMPTS; i++) {
191
206
  if (getAllMihomoPids().length === 0) break;
192
- utils.sleepSync(100);
207
+ utils.sleepSync(PROCESS_WAIT_INTERVAL);
193
208
  }
194
209
 
195
210
  clearPid();
@@ -281,7 +296,7 @@ function getProcessInfo(pid) {
281
296
  cpu: pcpu ? pcpu.toFixed(1) + '%' : '未知',
282
297
  isRoot: utils.isProcessRoot(pid),
283
298
  };
284
- } catch (e) {
299
+ } catch (_e) {
285
300
  return { pid, memory: '未知', cpu: '未知', isRoot: false };
286
301
  }
287
302
  }
@@ -367,7 +382,7 @@ async function startMixedMode(staleState) {
367
382
  const pid = child.pid;
368
383
  savePid(pid);
369
384
 
370
- await new Promise(resolve => setTimeout(resolve, 800));
385
+ await new Promise(resolve => setTimeout(resolve, STARTUP_WAIT_MS));
371
386
 
372
387
  if (!isRunning()) {
373
388
  clearPid();
@@ -416,12 +431,12 @@ async function startTunMode(staleState) {
416
431
  try {
417
432
  execSync('sudo "' + launchScript + '"', {
418
433
  stdio: 'inherit',
419
- timeout: 60000,
434
+ timeout: SUDO_TIMEOUT_MS,
420
435
  });
421
436
  } catch (e) {
422
437
  try {
423
438
  fs.unlinkSync(launchScript);
424
- } catch (e2) {}
439
+ } catch {}
425
440
  if (e.status === 1) {
426
441
  throw new Error('密码错误或取消');
427
442
  }
@@ -430,9 +445,9 @@ async function startTunMode(staleState) {
430
445
 
431
446
  try {
432
447
  fs.unlinkSync(launchScript);
433
- } catch (e) {}
448
+ } catch (_e) {}
434
449
 
435
- await new Promise(resolve => setTimeout(resolve, 500));
450
+ await new Promise(resolve => setTimeout(resolve, TUN_MODE_POST_WAIT_MS));
436
451
 
437
452
  const finalPid = getPid();
438
453
  if (!finalPid) {
@@ -468,7 +483,7 @@ function stop(forceSudo) {
468
483
 
469
484
  function rotateAndCleanupLogs() {
470
485
  rotateLog();
471
- cleanupOldLogs(7);
486
+ cleanupOldLogs(DEFAULT_LOG_RETENTION_DAYS);
472
487
  }
473
488
 
474
489
  function getLogPath() {
@@ -496,7 +511,7 @@ function rotateLog() {
496
511
  }
497
512
 
498
513
  function cleanupOldLogs(maxAgeDays) {
499
- if (maxAgeDays === undefined) maxAgeDays = 7;
514
+ if (maxAgeDays === undefined) maxAgeDays = DEFAULT_LOG_RETENTION_DAYS;
500
515
  const logsDir = config.DIRS.logs;
501
516
 
502
517
  if (!fs.existsSync(logsDir)) {
@@ -524,7 +539,7 @@ function cleanupOldLogs(maxAgeDays) {
524
539
  fs.unlinkSync(filePath);
525
540
  deleted++;
526
541
  }
527
- } catch (e) {
542
+ } catch (_e) {
528
543
  errors++;
529
544
  }
530
545
  }
@@ -570,7 +585,7 @@ function listLogs() {
570
585
  mtime: stat.mtime,
571
586
  isCurrent: false,
572
587
  });
573
- } catch (e) {
588
+ } catch (_e) {
574
589
  // ignore
575
590
  }
576
591
  }
@@ -588,7 +603,6 @@ function isPathUnderDir(filePath, baseDir) {
588
603
  function getLogPathByName(name) {
589
604
  const logsDir = config.DIRS.logs;
590
605
 
591
- // 处理部分匹配(用户只输入时间戳部分)
592
606
  let targetName = name;
593
607
  if (!name.endsWith('.log')) {
594
608
  targetName = 'mihomo.' + name + '.log';
@@ -602,7 +616,6 @@ function getLogPathByName(name) {
602
616
  return filePath;
603
617
  }
604
618
 
605
- // 尝试模糊匹配(readdirSync 返回的文件名已是安全的,但为了一致性仍校验)
606
619
  if (fs.existsSync(logsDir)) {
607
620
  const files = fs.readdirSync(logsDir);
608
621
  for (const file of files) {
@@ -622,12 +635,60 @@ function openUrl(url) {
622
635
  try {
623
636
  spawn('open', [url], { stdio: 'ignore', detached: true });
624
637
  return true;
625
- } catch (e) {
638
+ } catch (_e) {
626
639
  return false;
627
640
  }
628
641
  }
629
642
 
643
+ /**
644
+ * 打开日志文件(从 index.js 移入)
645
+ */
646
+ function openLogFile(logPath, label) {
647
+ const displayLabel = label || logPath;
648
+ console.log('用系统默认程序打开: ' + displayLabel);
649
+ const success = openUrl(logPath);
650
+ if (!success) {
651
+ console.log('请手动打开: ' + logPath);
652
+ }
653
+ }
654
+
655
+ /**
656
+ * 用 tail 查看日志(从 index.js 移入)
657
+ */
658
+ function viewLogWithTail(logPath, options) {
659
+ const follow = options && options.follow;
660
+ const lines = (options && options.lines) || 100;
661
+
662
+ console.log('日志: ' + logPath);
663
+ if (follow) {
664
+ console.log('按 Ctrl+C 退出\n');
665
+ } else {
666
+ console.log('显示最后 ' + lines + ' 行\n');
667
+ }
668
+
669
+ const tailArgs = [];
670
+ if (follow) tailArgs.push('-f');
671
+ tailArgs.push('-n', lines.toString());
672
+ tailArgs.push(logPath);
673
+
674
+ const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
675
+
676
+ tail.on('close', () => process.exit(0));
677
+ tail.on('error', e => {
678
+ console.error('无法读取日志: ' + e.message);
679
+ process.exit(1);
680
+ });
681
+ }
682
+
630
683
  module.exports = {
684
+ // 常量
685
+ PROCESS_WAIT_ATTEMPTS,
686
+ PROCESS_WAIT_INTERVAL,
687
+ STARTUP_WAIT_MS,
688
+ SUDO_TIMEOUT_MS,
689
+ TUN_MODE_POST_WAIT_MS,
690
+ DEFAULT_LOG_RETENTION_DAYS,
691
+ // 函数
631
692
  getAllMihomoPids,
632
693
  cleanupAll,
633
694
  getStatus,
@@ -637,4 +698,6 @@ module.exports = {
637
698
  listLogs,
638
699
  getLogPathByName,
639
700
  openUrl,
701
+ openLogFile,
702
+ viewLogWithTail,
640
703
  };