mihomo-cli 1.0.0-alpha.1 → 1.0.2
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 +92 -0
- package/README.md +96 -105
- package/index.js +436 -78
- package/package.json +14 -5
- package/src/config.js +129 -24
- package/src/kernel.js +6 -65
- package/src/process.js +151 -9
- package/src/subscription.js +159 -15
package/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const kernel = require('./src/kernel');
|
|
|
7
7
|
const subscription = require('./src/subscription');
|
|
8
8
|
const processMgr = require('./src/process');
|
|
9
9
|
|
|
10
|
-
const VERSION = '
|
|
10
|
+
const VERSION = require('./package.json').version;
|
|
11
11
|
|
|
12
12
|
const UI_URLS = {
|
|
13
13
|
zash: 'https://board.zash.run.place',
|
|
@@ -45,23 +45,47 @@ process.on('unhandledRejection', (reason) => {
|
|
|
45
45
|
process.exit(1);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
function printShortHelp() {
|
|
49
|
+
console.log('\nmihomo-cli v' + VERSION);
|
|
50
|
+
console.log('别名: mihomo, mmc, mh\n');
|
|
51
|
+
console.log('命令:\n' +
|
|
52
|
+
' start [tun|mixed] 启动/切换代理(重复执行可切换模式)\n' +
|
|
53
|
+
' stop 停止代理\n' +
|
|
54
|
+
' status 查看状态\n' +
|
|
55
|
+
' log 实时日志\n' +
|
|
56
|
+
' logs 历史日志\n' +
|
|
57
|
+
' ui [zash|dash|yacd] Web 界面\n' +
|
|
58
|
+
' kernel 更新内核\n' +
|
|
59
|
+
' sub add <url> [name] 添加订阅\n' +
|
|
60
|
+
' sub update [name] 更新订阅(无参更新所有)\n' +
|
|
61
|
+
' sub use <name> 切换默认订阅\n' +
|
|
62
|
+
' sub web [name] 打开订阅页面\n' +
|
|
63
|
+
' sub list 列出订阅\n' +
|
|
64
|
+
' reset 重置配置\n' +
|
|
65
|
+
' dirs 数据目录\n' +
|
|
66
|
+
' version 版本信息\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
48
69
|
function printHelp() {
|
|
49
70
|
console.log('\nmihomo-cli v' + VERSION + '\n' +
|
|
71
|
+
'\n' +
|
|
72
|
+
'命令别名: mihomo, mmc, mh\n' +
|
|
50
73
|
'\n' +
|
|
51
74
|
'用法:\n' +
|
|
52
75
|
' mihomo-cli <命令> [选项]\n' +
|
|
53
76
|
'\n' +
|
|
54
77
|
'命令:\n' +
|
|
55
|
-
' start [tun|mixed]
|
|
78
|
+
' start [tun|mixed] 启动/切换代理 (默认 mixed, 重复执行可重启/切换模式)\n' +
|
|
56
79
|
' stop 停止代理\n' +
|
|
57
|
-
' restart [tun|mixed] 重启代理\n' +
|
|
58
80
|
' status 查看状态\n' +
|
|
59
81
|
' log 实时日志\n' +
|
|
82
|
+
' logs [name] [-n N] 列出/查看历史日志 (默认 100 行)\n' +
|
|
60
83
|
' ui [zash|dash|yacd] 打开 Web UI (默认 zash)\n' +
|
|
61
|
-
'
|
|
62
|
-
' kernel 更新内核\n' +
|
|
84
|
+
' kernel [镜像] 更新内核 (可指定镜像: hk.gh-proxy.org 或 --no-mirror)\n' +
|
|
63
85
|
' sub add <url> [name] 添加订阅\n' +
|
|
64
|
-
' sub update [name]
|
|
86
|
+
' sub update [name] 更新订阅 (无参更新所有)\n' +
|
|
87
|
+
' sub use <name> 设置默认订阅 (支持模糊匹配)\n' +
|
|
88
|
+
' sub web [name] 打开订阅页面\n' +
|
|
65
89
|
' sub list 列出订阅\n' +
|
|
66
90
|
' reset [--full] 重置用户数据 (--full 同时删除内核)\n' +
|
|
67
91
|
' dirs 显示数据目录位置\n' +
|
|
@@ -69,8 +93,8 @@ function printHelp() {
|
|
|
69
93
|
' version, -v 显示版本\n' +
|
|
70
94
|
'\n' +
|
|
71
95
|
'示例:\n' +
|
|
72
|
-
' mihomo-cli start #
|
|
73
|
-
' mihomo-cli start tun #
|
|
96
|
+
' mihomo-cli start # 启动/重启 Mixed 模式\n' +
|
|
97
|
+
' mihomo-cli start tun # 启动/切换到 TUN 模式 (透明代理)\n' +
|
|
74
98
|
' mihomo-cli ui # 打开默认 UI (zash)\n' +
|
|
75
99
|
' mihomo-cli ui dash # 打开 metacubexd\n' +
|
|
76
100
|
' mihomo-cli ui yacd # 打开 YACD\n' +
|
|
@@ -123,6 +147,103 @@ function getActiveSubscription() {
|
|
|
123
147
|
return subs[0];
|
|
124
148
|
}
|
|
125
149
|
|
|
150
|
+
function findSubsFuzzy(subs, pattern) {
|
|
151
|
+
const lowerPattern = pattern.toLowerCase();
|
|
152
|
+
let exact = [];
|
|
153
|
+
let prefix = [];
|
|
154
|
+
let includes = [];
|
|
155
|
+
|
|
156
|
+
for (const s of subs) {
|
|
157
|
+
const name = s.name.toLowerCase();
|
|
158
|
+
if (name === lowerPattern) {
|
|
159
|
+
exact.push(s);
|
|
160
|
+
} else if (name.startsWith(lowerPattern)) {
|
|
161
|
+
prefix.push(s);
|
|
162
|
+
} else if (name.includes(lowerPattern)) {
|
|
163
|
+
includes.push(s);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (exact.length > 0) return exact;
|
|
168
|
+
if (prefix.length > 0) return prefix;
|
|
169
|
+
return includes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function pickSingleSub(subs, pattern, actionName) {
|
|
173
|
+
if (subs.length === 0) {
|
|
174
|
+
console.error(' 错误: 未找到匹配 "' + pattern + '" 的订阅');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
if (subs.length === 1) {
|
|
178
|
+
return subs[0];
|
|
179
|
+
}
|
|
180
|
+
console.error(' 错误: 匹配到多个订阅,请更精确指定');
|
|
181
|
+
console.log('\n 匹配的订阅:');
|
|
182
|
+
subs.forEach(s => console.log(' ' + s.name));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function hasFlag(args, short, long) {
|
|
187
|
+
return args && (args.includes(short) || args.includes(long));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function parseIntArg(args, short, long, defaultValue) {
|
|
191
|
+
if (!args) return defaultValue;
|
|
192
|
+
for (let i = 0; i < args.length; i++) {
|
|
193
|
+
if (args[i] === short || args[i] === long) {
|
|
194
|
+
if (i + 1 < args.length) {
|
|
195
|
+
const val = parseInt(args[i + 1]);
|
|
196
|
+
return isNaN(val) ? defaultValue : val;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return defaultValue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function getNonFlagArg(args, startIdx) {
|
|
204
|
+
if (!args) return null;
|
|
205
|
+
for (let i = startIdx; i < args.length; i++) {
|
|
206
|
+
if (!args[i].startsWith('-')) {
|
|
207
|
+
return args[i];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function openLogFile(logPath, label) {
|
|
214
|
+
const displayLabel = label || logPath;
|
|
215
|
+
console.log(' 用系统默认程序打开: ' + displayLabel);
|
|
216
|
+
const success = processMgr.openUrl(logPath);
|
|
217
|
+
if (!success) {
|
|
218
|
+
console.log(' 请手动打开: ' + logPath);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function viewLogWithTail(logPath, options) {
|
|
223
|
+
const follow = options && options.follow;
|
|
224
|
+
const lines = (options && options.lines) || 100;
|
|
225
|
+
|
|
226
|
+
console.log(' 日志: ' + logPath);
|
|
227
|
+
if (follow) {
|
|
228
|
+
console.log(' 按 Ctrl+C 退出\n');
|
|
229
|
+
} else {
|
|
230
|
+
console.log(' 显示最后 ' + lines + ' 行\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const tailArgs = [];
|
|
234
|
+
if (follow) tailArgs.push('-f');
|
|
235
|
+
tailArgs.push('-n', lines.toString());
|
|
236
|
+
tailArgs.push(logPath);
|
|
237
|
+
|
|
238
|
+
const tail = spawn('tail', tailArgs, { stdio: 'inherit' });
|
|
239
|
+
|
|
240
|
+
tail.on('close', () => process.exit(0));
|
|
241
|
+
tail.on('error', (e) => {
|
|
242
|
+
console.error(' 无法读取日志: ' + e.message);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
126
247
|
async function cmdStart(args) {
|
|
127
248
|
if (!config.hasKernel()) {
|
|
128
249
|
console.error(' 错误: 未找到内核,请运行 \'mihomo-cli kernel\'');
|
|
@@ -137,14 +258,28 @@ async function cmdStart(args) {
|
|
|
137
258
|
process.exit(1);
|
|
138
259
|
}
|
|
139
260
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
261
|
+
await subscription.autoUpdateStaleSubscriptions();
|
|
262
|
+
|
|
263
|
+
// 每次 start 都先确保完全干净的状态(停止进程 + 清理运行时文件)
|
|
264
|
+
const status = processMgr.getStatus();
|
|
265
|
+
const hasProcess = status.running || status.allProcesses.length > 0;
|
|
266
|
+
|
|
267
|
+
if (hasProcess) {
|
|
268
|
+
const count = status.allProcesses.length > 0 ? status.allProcesses.length : 1;
|
|
269
|
+
console.log(' 停止 ' + count + ' 个进程...');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 总是调用 stop(即使没进程也会清理 PID 文件和运行时目录)
|
|
273
|
+
const stopResult = processMgr.stop(true);
|
|
274
|
+
|
|
275
|
+
if (stopResult.remaining && stopResult.remaining.length > 0) {
|
|
276
|
+
console.error(' 部分进程无法终止: ' + stopResult.remaining.join(', '));
|
|
277
|
+
console.error(' 请手动运行: sudo pkill -9 mihomo');
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (hasProcess) {
|
|
282
|
+
console.log(' 已停止\n');
|
|
148
283
|
}
|
|
149
284
|
|
|
150
285
|
let cfgInfo;
|
|
@@ -169,36 +304,22 @@ async function cmdStart(args) {
|
|
|
169
304
|
}
|
|
170
305
|
}
|
|
171
306
|
|
|
172
|
-
function
|
|
307
|
+
async function cmdStop() {
|
|
173
308
|
const pids = processMgr.getAllMihomoPids();
|
|
174
309
|
if (pids.length === 0) {
|
|
175
310
|
console.log(' 未在运行');
|
|
176
|
-
return
|
|
311
|
+
return;
|
|
177
312
|
}
|
|
178
313
|
|
|
179
314
|
console.log(' 停止 ' + pids.length + ' 个进程...');
|
|
180
315
|
const result = processMgr.stop(true);
|
|
181
316
|
|
|
182
317
|
if (result.remaining && result.remaining.length > 0) {
|
|
183
|
-
console.
|
|
184
|
-
console.
|
|
185
|
-
return { hasRunning: true, success: false, remaining: result.remaining };
|
|
186
|
-
}
|
|
187
|
-
console.log(' 已停止');
|
|
188
|
-
return { hasRunning: true, success: true };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function cmdStop() {
|
|
192
|
-
const result = doStop();
|
|
193
|
-
if (!result.success) {
|
|
318
|
+
console.error(' 部分进程未终止: ' + result.remaining.join(', '));
|
|
319
|
+
console.error(' 请手动运行: sudo pkill -9 mihomo');
|
|
194
320
|
process.exit(1);
|
|
195
321
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
async function cmdRestart(args) {
|
|
199
|
-
const stopResult = doStop();
|
|
200
|
-
console.log('');
|
|
201
|
-
await cmdStart(args);
|
|
322
|
+
console.log(' 已停止');
|
|
202
323
|
}
|
|
203
324
|
|
|
204
325
|
function cmdUi(args) {
|
|
@@ -220,42 +341,167 @@ function cmdUi(args) {
|
|
|
220
341
|
}
|
|
221
342
|
}
|
|
222
343
|
|
|
223
|
-
function cmdClean() {
|
|
224
|
-
console.log(' 清理残留进程...');
|
|
225
|
-
const result = processMgr.cleanupAll();
|
|
226
344
|
|
|
227
|
-
|
|
228
|
-
|
|
345
|
+
function cmdLog(args) {
|
|
346
|
+
const logPath = processMgr.getLogPath();
|
|
347
|
+
|
|
348
|
+
if (hasFlag(args, '-o', '--open')) {
|
|
349
|
+
openLogFile(logPath);
|
|
350
|
+
return;
|
|
229
351
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
352
|
+
|
|
353
|
+
viewLogWithTail(logPath, { follow: true, lines: 50 });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function cmdLogs(args) {
|
|
357
|
+
const targetName = getNonFlagArg(args, 1);
|
|
358
|
+
const lines = parseIntArg(args, '-n', '--lines', 100);
|
|
359
|
+
const openInViewer = hasFlag(args, '-o', '--open');
|
|
360
|
+
|
|
361
|
+
if (targetName) {
|
|
362
|
+
let logPath;
|
|
363
|
+
|
|
364
|
+
if (targetName === 'current' || targetName === '0') {
|
|
365
|
+
logPath = processMgr.getLogPath();
|
|
366
|
+
} else {
|
|
367
|
+
logPath = processMgr.getLogPathByName(targetName);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!logPath) {
|
|
371
|
+
console.error(' 错误: 未找到日志 "' + targetName + '"');
|
|
372
|
+
console.log(' 使用 "mihomo-cli logs" 查看可用日志列表');
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (openInViewer) {
|
|
377
|
+
openLogFile(logPath);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
viewLogWithTail(logPath, { follow: false, lines });
|
|
382
|
+
return;
|
|
234
383
|
}
|
|
235
|
-
|
|
236
|
-
|
|
384
|
+
|
|
385
|
+
const logs = processMgr.listLogs();
|
|
386
|
+
const all = [];
|
|
387
|
+
|
|
388
|
+
if (logs.current) {
|
|
389
|
+
all.push(logs.current);
|
|
237
390
|
}
|
|
238
|
-
|
|
391
|
+
all.push(...logs.archives);
|
|
239
392
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
393
|
+
if (all.length === 0) {
|
|
394
|
+
console.log(' 暂无日志');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
244
397
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
398
|
+
console.log('');
|
|
399
|
+
console.log(' 日志列表:');
|
|
400
|
+
console.log('');
|
|
248
401
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
402
|
+
all.forEach((log, idx) => {
|
|
403
|
+
const num = log.isCurrent ? ' 0' : (idx < 10 ? ' ' + idx : '' + idx);
|
|
404
|
+
const time = subscription.formatDate(log.mtime);
|
|
405
|
+
const size = subscription.formatBytes(log.size);
|
|
406
|
+
const name = log.isCurrent ? 'mihomo.log (当前运行中)' : log.name;
|
|
407
|
+
|
|
408
|
+
console.log(' ' + num + '. ' + name);
|
|
409
|
+
console.log(' 时间: ' + time + ' 大小: ' + size);
|
|
410
|
+
if (!log.isCurrent) {
|
|
411
|
+
console.log(' 查看: mihomo-cli logs ' + idx + ' 或 mihomo-cli logs -o ' + idx);
|
|
412
|
+
}
|
|
413
|
+
console.log('');
|
|
253
414
|
});
|
|
415
|
+
|
|
416
|
+
console.log(' 用法:');
|
|
417
|
+
console.log(' mihomo-cli logs 0 # 查看当前日志 (最后 100 行)');
|
|
418
|
+
console.log(' mihomo-cli logs 1 # 查看第 1 个归档日志');
|
|
419
|
+
console.log(' mihomo-cli logs 1 -n 200 # 查看 200 行');
|
|
420
|
+
console.log(' mihomo-cli logs 1 -o # 用系统默认程序打开');
|
|
421
|
+
console.log('');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// 解析镜像参数
|
|
425
|
+
function parseMirrorArg(args) {
|
|
426
|
+
// 返回: { mirror: 镜像URL|null, isOverride: boolean }
|
|
427
|
+
// mirror = null 表示禁用镜像
|
|
428
|
+
// mirror = undefined 表示使用默认/配置
|
|
429
|
+
|
|
430
|
+
if (!args || args.length < 2) {
|
|
431
|
+
return { mirror: undefined, isOverride: false };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 检查 --no-mirror
|
|
435
|
+
if (args.includes('--no-mirror') || args.includes('--direct')) {
|
|
436
|
+
return { mirror: null, isOverride: true };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 检查 --mirror <值>
|
|
440
|
+
const mirrorIdx = args.indexOf('--mirror');
|
|
441
|
+
if (mirrorIdx >= 0 && mirrorIdx + 1 < args.length) {
|
|
442
|
+
let mirrorVal = args[mirrorIdx + 1];
|
|
443
|
+
return { mirror: normalizeMirrorUrl(mirrorVal), isOverride: true };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 第一个非 flag 参数作为镜像
|
|
447
|
+
for (let i = 1; i < args.length; i++) {
|
|
448
|
+
const arg = args[i];
|
|
449
|
+
if (!arg.startsWith('-')) {
|
|
450
|
+
return { mirror: normalizeMirrorUrl(arg), isOverride: true };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return { mirror: undefined, isOverride: false };
|
|
254
455
|
}
|
|
255
456
|
|
|
256
|
-
|
|
457
|
+
function normalizeMirrorUrl(val) {
|
|
458
|
+
if (!val) return null;
|
|
459
|
+
if (val === 'direct' || val === 'no' || val === 'none') return null;
|
|
460
|
+
|
|
461
|
+
let url = val;
|
|
462
|
+
if (!url.startsWith('http')) {
|
|
463
|
+
url = 'https://' + url;
|
|
464
|
+
}
|
|
465
|
+
if (!url.endsWith('/')) {
|
|
466
|
+
url += '/';
|
|
467
|
+
}
|
|
468
|
+
return url;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function cmdKernel(args) {
|
|
472
|
+
const mirrorInfo = parseMirrorArg(args);
|
|
473
|
+
const effectiveMirror = mirrorInfo.isOverride ? mirrorInfo.mirror : config.getGitHubMirror();
|
|
474
|
+
const isDefault = !mirrorInfo.isOverride && effectiveMirror === config.DEFAULT_GITHUB_MIRROR;
|
|
475
|
+
|
|
257
476
|
console.log(' 检查内核更新...');
|
|
258
477
|
|
|
478
|
+
if (mirrorInfo.isOverride) {
|
|
479
|
+
if (effectiveMirror === null) {
|
|
480
|
+
console.log(' 镜像: 直连(命令行指定 --no-mirror)');
|
|
481
|
+
} else {
|
|
482
|
+
console.log(' 镜像: ' + effectiveMirror + ' (命令行指定)');
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
console.log(' 镜像: ' + (effectiveMirror || '直连(无镜像)') + (isDefault && effectiveMirror ? ' (默认)' : ''));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log('\n 可用镜像:');
|
|
489
|
+
config.AVAILABLE_MIRRORS.forEach(m => {
|
|
490
|
+
const isCurrent = effectiveMirror && (
|
|
491
|
+
effectiveMirror.includes('//' + m + '/') ||
|
|
492
|
+
effectiveMirror.includes('//' + m + ':') ||
|
|
493
|
+
effectiveMirror.endsWith('//' + m)
|
|
494
|
+
);
|
|
495
|
+
console.log(' ' + m + (isCurrent ? ' (当前)' : ''));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
console.log('\n 用法:');
|
|
499
|
+
console.log(' mihomo-cli kernel # 使用默认镜像');
|
|
500
|
+
console.log(' mihomo-cli kernel hk.gh-proxy.org # 使用指定镜像');
|
|
501
|
+
console.log(' mihomo-cli kernel --mirror hk.gh-proxy.org');
|
|
502
|
+
console.log(' mihomo-cli kernel --no-mirror # 直连,不使用镜像');
|
|
503
|
+
console.log('');
|
|
504
|
+
|
|
259
505
|
try {
|
|
260
506
|
const info = await kernel.checkUpdate();
|
|
261
507
|
console.log(' 当前: ' + info.current);
|
|
@@ -269,7 +515,7 @@ async function cmdKernel() {
|
|
|
269
515
|
console.log('\n 正在下载...');
|
|
270
516
|
const result = await kernel.downloadKernel((msg) => {
|
|
271
517
|
console.log(' ' + msg);
|
|
272
|
-
});
|
|
518
|
+
}, mirrorInfo.mirror); // 传递镜像参数(undefined = 用配置,null = 禁用)
|
|
273
519
|
console.log(' 已更新到 ' + result.version);
|
|
274
520
|
} catch (e) {
|
|
275
521
|
console.error(' 更新失败: ' + e.message);
|
|
@@ -281,7 +527,12 @@ async function cmdSub(args) {
|
|
|
281
527
|
const action = args[1];
|
|
282
528
|
|
|
283
529
|
if (!action || action === 'list') {
|
|
284
|
-
const
|
|
530
|
+
const updateResult = await subscription.autoUpdateStaleSubscriptions();
|
|
531
|
+
if (updateResult.total > 0) {
|
|
532
|
+
console.log('');
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const subs = config.getSubscriptionsWithCache();
|
|
285
536
|
if (subs.length === 0) {
|
|
286
537
|
console.log(' 没有订阅');
|
|
287
538
|
console.log('\n 添加订阅:');
|
|
@@ -290,11 +541,39 @@ async function cmdSub(args) {
|
|
|
290
541
|
}
|
|
291
542
|
console.log(' 订阅列表:');
|
|
292
543
|
subs.forEach((s, i) => {
|
|
293
|
-
const time =
|
|
294
|
-
|
|
544
|
+
const time = subscription.formatDate(s.updatedAt);
|
|
545
|
+
const defaultMark = i === 0 ? ' [默认]' : '';
|
|
546
|
+
const interval = s.updateInterval || subscription.DEFAULT_UPDATE_INTERVAL_HOURS;
|
|
547
|
+
console.log(' ' + (i + 1) + '. ' + s.name + defaultMark);
|
|
548
|
+
console.log(' 更新: ' + time + ' (间隔: ' + interval + 'h)');
|
|
549
|
+
|
|
550
|
+
if (s.username) {
|
|
551
|
+
console.log(' 用户: ' + s.username);
|
|
552
|
+
}
|
|
553
|
+
if (s.download !== undefined || s.total !== undefined) {
|
|
554
|
+
const used = (s.upload || 0) + (s.download || 0);
|
|
555
|
+
const usedStr = subscription.formatBytes(used);
|
|
556
|
+
const totalStr = subscription.formatBytes(s.total);
|
|
557
|
+
let percentStr = '';
|
|
558
|
+
if (s.total && s.total > 0) {
|
|
559
|
+
const percent = Math.min((used / s.total) * 100, 100);
|
|
560
|
+
percentStr = ' (' + percent.toFixed(1) + '%)';
|
|
561
|
+
}
|
|
562
|
+
console.log(' 流量: ' + usedStr + ' / ' + totalStr + percentStr);
|
|
563
|
+
}
|
|
564
|
+
if (s.expire !== undefined) {
|
|
565
|
+
console.log(' 到期: ' + subscription.formatTimestamp(s.expire));
|
|
566
|
+
}
|
|
567
|
+
if (s.webPageUrl) {
|
|
568
|
+
console.log(' 页面: ' + s.webPageUrl);
|
|
569
|
+
}
|
|
295
570
|
});
|
|
296
|
-
console.log('\n
|
|
571
|
+
console.log('\n 切换默认订阅:');
|
|
572
|
+
console.log(' mihomo-cli sub use <name>');
|
|
573
|
+
console.log(' 更新订阅:');
|
|
297
574
|
console.log(' mihomo-cli sub update [name]');
|
|
575
|
+
console.log(' 打开订阅页面:');
|
|
576
|
+
console.log(' mihomo-cli sub web [name]');
|
|
298
577
|
return;
|
|
299
578
|
}
|
|
300
579
|
|
|
@@ -328,12 +607,25 @@ async function cmdSub(args) {
|
|
|
328
607
|
process.exit(1);
|
|
329
608
|
}
|
|
330
609
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
610
|
+
if (!name) {
|
|
611
|
+
console.log(' 更新所有 ' + subs.length + ' 个订阅...');
|
|
612
|
+
const results = await Promise.all(subs.map(subscription.tryUpdateOne));
|
|
613
|
+
let ok = 0;
|
|
614
|
+
results.forEach(r => {
|
|
615
|
+
if (r.success) {
|
|
616
|
+
ok++;
|
|
617
|
+
console.log(' ✓ ' + r.name + ': ' + r.proxies + ' 节点');
|
|
618
|
+
} else {
|
|
619
|
+
console.log(' ✗ ' + r.name + ': 失败 (' + r.error.split('\n')[0] + ')');
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
if (ok === 0) process.exit(1);
|
|
623
|
+
return;
|
|
335
624
|
}
|
|
336
625
|
|
|
626
|
+
const matches = findSubsFuzzy(subs, name);
|
|
627
|
+
const target = pickSingleSub(matches, name, '更新');
|
|
628
|
+
|
|
337
629
|
console.log(' 更新订阅: ' + target.name);
|
|
338
630
|
try {
|
|
339
631
|
const info = await subscription.downloadSubscription(target.url, target.name);
|
|
@@ -345,8 +637,76 @@ async function cmdSub(args) {
|
|
|
345
637
|
return;
|
|
346
638
|
}
|
|
347
639
|
|
|
640
|
+
if (action === 'use') {
|
|
641
|
+
const name = args[2];
|
|
642
|
+
const subs = config.getSubscriptions();
|
|
643
|
+
|
|
644
|
+
if (!name) {
|
|
645
|
+
console.error(' 错误: 请指定订阅名称');
|
|
646
|
+
if (subs.length > 0) {
|
|
647
|
+
console.log('\n 可用订阅:');
|
|
648
|
+
subs.forEach(s => console.log(' ' + s.name));
|
|
649
|
+
}
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const matches = findSubsFuzzy(subs, name);
|
|
654
|
+
const target = pickSingleSub(matches, name, '切换');
|
|
655
|
+
|
|
656
|
+
const success = config.setDefaultSubscription(target.name);
|
|
657
|
+
if (success) {
|
|
658
|
+
console.log(' 已设置 "' + target.name + '" 为默认订阅');
|
|
659
|
+
} else {
|
|
660
|
+
console.error(' 错误: 未找到订阅 "' + name + '"');
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (action === 'web' || action === 'open') {
|
|
667
|
+
const name = args[2];
|
|
668
|
+
const subs = config.getSubscriptionsWithCache();
|
|
669
|
+
|
|
670
|
+
if (subs.length === 0) {
|
|
671
|
+
console.error(' 错误: 没有订阅');
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
let target;
|
|
676
|
+
if (name) {
|
|
677
|
+
const matches = findSubsFuzzy(subs, name);
|
|
678
|
+
target = pickSingleSub(matches, name, '打开');
|
|
679
|
+
} else {
|
|
680
|
+
target = subs[0];
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
let webPageUrl = target.webPageUrl;
|
|
684
|
+
if (!webPageUrl) {
|
|
685
|
+
console.log(' 订阅信息中缺少页面地址,正在更新订阅...');
|
|
686
|
+
try {
|
|
687
|
+
const info = await subscription.downloadSubscription(target.url, target.name);
|
|
688
|
+
if (info.webPageUrl) {
|
|
689
|
+
webPageUrl = info.webPageUrl;
|
|
690
|
+
} else {
|
|
691
|
+
console.error(' 错误: 该订阅没有提供页面地址');
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
} catch (e) {
|
|
695
|
+
console.error(' 更新失败: ' + e.message);
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
console.log(' 打开订阅页面: ' + webPageUrl);
|
|
701
|
+
const opened = processMgr.openUrl(webPageUrl);
|
|
702
|
+
if (!opened) {
|
|
703
|
+
console.log(' 请手动访问上面的地址');
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
348
708
|
console.error(' 错误: 未知的订阅命令');
|
|
349
|
-
console.log(' 用法: mihomo-cli sub [list|add|update]');
|
|
709
|
+
console.log(' 用法: mihomo-cli sub [list|add|update|use|web]');
|
|
350
710
|
process.exit(1);
|
|
351
711
|
}
|
|
352
712
|
|
|
@@ -417,7 +777,8 @@ async function main() {
|
|
|
417
777
|
const args = process.argv.slice(2);
|
|
418
778
|
|
|
419
779
|
if (args.length === 0) {
|
|
420
|
-
|
|
780
|
+
printStatus();
|
|
781
|
+
printShortHelp();
|
|
421
782
|
return;
|
|
422
783
|
}
|
|
423
784
|
|
|
@@ -439,23 +800,20 @@ async function main() {
|
|
|
439
800
|
case 'stop':
|
|
440
801
|
await cmdStop();
|
|
441
802
|
break;
|
|
442
|
-
case 'restart':
|
|
443
|
-
await cmdRestart(args);
|
|
444
|
-
break;
|
|
445
803
|
case 'status':
|
|
446
804
|
printStatus();
|
|
447
805
|
break;
|
|
448
806
|
case 'log':
|
|
449
|
-
cmdLog();
|
|
807
|
+
cmdLog(args);
|
|
808
|
+
break;
|
|
809
|
+
case 'logs':
|
|
810
|
+
cmdLogs(args);
|
|
450
811
|
break;
|
|
451
812
|
case 'ui':
|
|
452
813
|
cmdUi(args);
|
|
453
814
|
break;
|
|
454
|
-
case 'clean':
|
|
455
|
-
cmdClean();
|
|
456
|
-
break;
|
|
457
815
|
case 'kernel':
|
|
458
|
-
await cmdKernel();
|
|
816
|
+
await cmdKernel(args);
|
|
459
817
|
break;
|
|
460
818
|
case 'sub':
|
|
461
819
|
case 'subscription':
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mihomo-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A terminal-based mihomo (Clash.Meta) client for macOS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mihomo-cli": "
|
|
7
|
+
"mihomo-cli": "index.js",
|
|
8
|
+
"mihomo": "index.js",
|
|
9
|
+
"mmc": "index.js",
|
|
10
|
+
"mh": "index.js"
|
|
8
11
|
},
|
|
9
12
|
"files": [
|
|
10
13
|
"index.js",
|
|
11
|
-
"src/**/*.js"
|
|
14
|
+
"src/**/*.js",
|
|
15
|
+
"CHANGELOG.md"
|
|
12
16
|
],
|
|
13
17
|
"scripts": {
|
|
14
18
|
"start": "node index.js"
|
|
@@ -27,8 +31,13 @@
|
|
|
27
31
|
"engines": {
|
|
28
32
|
"node": ">=18.0.0"
|
|
29
33
|
},
|
|
30
|
-
"os": [
|
|
31
|
-
|
|
34
|
+
"os": [
|
|
35
|
+
"darwin"
|
|
36
|
+
],
|
|
37
|
+
"cpu": [
|
|
38
|
+
"x64",
|
|
39
|
+
"arm64"
|
|
40
|
+
],
|
|
32
41
|
"dependencies": {
|
|
33
42
|
"axios": "^1.6.0",
|
|
34
43
|
"compare-versions": "^6.1.0",
|