mihomo-cli 3.5.0 → 3.6.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/CHANGELOG.md +21 -0
- package/README.md +5 -4
- package/dist/index.js +285 -186
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.6.0] - 2026-08-15
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
|
|
7
|
+
- **`sub add` 重名时误删既有同名订阅** - 回滚逻辑包住了入库步骤,添加重名订阅时「已存在」错误同样触发 `removeSubscription`,把用户既有的同名订阅(含缓存与配置文件)删掉。现入库移出 try 块,回滚仅覆盖「入库成功后下载失败」
|
|
8
|
+
- **`start` 拼错模式名静默按 Mixed 启动** - `mihomo start tn` 之类只判 `=== 'tun'`,拼错不报错,用户会误以为已切到 TUN。现校验模式参数并报错,参数校验先于内核/订阅环境检查
|
|
9
|
+
- **pidFile 运行判定可被 PID 复用欺骗** - 只检查 pid 存活,系统重启后残留 pid 文件里的 pid 可能已被无关进程复用,被误判成运行中的 mihomo。现同时校验进程命令行包含内核路径(与测速实例防护同口径)
|
|
10
|
+
- **数据层预期错误打印堆栈** - 订阅名称非法/重名、未找到订阅配置、无有效节点、内核/配置缺失等用户可见的预期错误仍抛裸 `Error`,经统一收口后打印堆栈。现全部迁移为 `CliError`
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
|
|
14
|
+
- **did-you-mean 纠错** - 未知命令与 `sub`/`daemon` 未知子命令按前缀 + 编辑距离给出纠错建议(如 `mihomo strt` -> `是否想输入: start / stop?`)
|
|
15
|
+
- **`update` 更新前检查最新版** - 先查 npm registry,已是最新版本则跳过重装;查询失败(15s 超时)降级为直接安装
|
|
16
|
+
- **`status` 显示订阅流量与到期** - 复用订阅缓存的响应头数据,有则展示
|
|
17
|
+
- **场景化提醒** - `ui` 在 mihomo 未运行时提示先启动;`kernel` 更新成功后运行中实例提示需重启生效
|
|
18
|
+
|
|
19
|
+
### 内部
|
|
20
|
+
|
|
21
|
+
- **utils 按职责拆分** - `colors.ts`(颜色)、`errors.ts`(CliError/TimeoutError/withTimeout)、`http.ts`(HTTP 客户端)独立成模块;`isProcess*` 进程探测归入 `process.ts`,`isProxyValid` 归入其唯一消费者 `test-instance.ts`;`VERSION`/`PKG_NAME` 移入 `constants.ts`。`utils.ts` 只留无副作用的纯函数(sleep、转义、格式化、flag 解析、纠错建议)
|
|
22
|
+
- **补充单元测试** - 新增 `suggestSimilar`(did-you-mean)单测,总计 55 个
|
|
23
|
+
|
|
3
24
|
## [3.5.0] - 2026-08-15
|
|
4
25
|
|
|
5
26
|
### 变更
|
package/README.md
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
- 🚀 **进程管理** - 启动/停止/切换模式,自动清理残留进程
|
|
14
14
|
- 🛡️ **进程保活** - 基于 launchd(root),崩溃/开机自动拉起,代理后台常驻(`daemon on`)
|
|
15
15
|
- 🔄 **双模式支持** - Mixed 模式和 TUN 透明代理模式
|
|
16
|
-
- 📊 **状态监控** -
|
|
16
|
+
- 📊 **状态监控** - 查看运行状态、内存占用、订阅流量与到期时间
|
|
17
17
|
- 📝 **日志管理** - 实时日志 + 历史日志归档(自动轮转,保留7天)
|
|
18
18
|
- 🎨 **Web UI** - 一键打开 Web 控制面板 (zash/metacubexd/yacd)
|
|
19
19
|
- 🔄 **内核更新** - 自动检查更新,支持 GitHub 镜像加速
|
|
20
|
+
- 💡 **容错提示** - 命令/子命令拼错时给出 did-you-mean 纠错建议
|
|
20
21
|
- ⌨️ **命令别名** - `mihomo` / `mhm` / `mh` 均可调用
|
|
21
22
|
|
|
22
23
|
## 安装
|
|
@@ -86,7 +87,7 @@ mihomo ui yacd # YACD
|
|
|
86
87
|
| --------------------------- | ---------------------------------------------------------------------------- |
|
|
87
88
|
| `mihomo start [tun\|mixed]` | 启动/重启/切换代理模式(`-s` 跳过更新,`-u` 更新超时,`-r` 清理轮次,`-t` 超时,`-j` 并发,`--no-clean` 跳过启动自动清理) |
|
|
88
89
|
| `mihomo stop` | 停止代理 |
|
|
89
|
-
| `mihomo status` |
|
|
90
|
+
| `mihomo status` | 查看运行状态(含订阅流量、到期时间) |
|
|
90
91
|
| `mihomo log` | 实时查看日志 (`-o` 用系统编辑器打开) |
|
|
91
92
|
| `mihomo logs` | 列出所有日志(当前 + 历史归档) |
|
|
92
93
|
| `mihomo logs <编号>` | 查看指定日志(`0`=当前日志,`1+`=归档日志,支持 `-n N` 指定行数、`-o` 打开) |
|
|
@@ -119,9 +120,9 @@ mihomo ui yacd # YACD
|
|
|
119
120
|
|
|
120
121
|
| 命令 | 说明 |
|
|
121
122
|
| --------------------------------- | ------------------------------------------------------------------- |
|
|
122
|
-
| `mihomo kernel [--mirror [镜像]]` | 更新内核(默认直连,`--mirror`
|
|
123
|
+
| `mihomo kernel [--mirror [镜像]]` | 更新内核(默认直连,`--mirror` 使用镜像;更新后运行中实例需重启生效) |
|
|
123
124
|
| `mihomo daemon [on\|off\|status]` | 进程保活:开机自启 + 崩溃自动重启(仅 Mixed 模式,on/off 需管理员密码) |
|
|
124
|
-
| `mihomo update` | 更新 mihomo-cli
|
|
125
|
+
| `mihomo update` | 更新 mihomo-cli(先查 npm 最新版,已是最新则跳过重装) |
|
|
125
126
|
| `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
|
|
126
127
|
| `mihomo dir` | 显示数据目录位置 |
|
|
127
128
|
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `kernel` 等) |
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/colors.ts
|
|
4
|
+
var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
5
|
+
function colorize(code, str) {
|
|
6
|
+
if (NO_COLOR) return String(str);
|
|
7
|
+
return `${code + String(str)}\x1B[0m`;
|
|
8
|
+
}
|
|
9
|
+
var colors = {
|
|
10
|
+
bold: (s) => colorize("\x1B[1m", s),
|
|
11
|
+
red: (s) => colorize("\x1B[31m", s),
|
|
12
|
+
green: (s) => colorize("\x1B[32m", s),
|
|
13
|
+
yellow: (s) => colorize("\x1B[33m", s),
|
|
14
|
+
cyan: (s) => colorize("\x1B[36m", s),
|
|
15
|
+
gray: (s) => colorize("\x1B[90m", s)
|
|
16
|
+
};
|
|
17
|
+
|
|
3
18
|
// src/config.ts
|
|
4
|
-
import { spawnSync
|
|
19
|
+
import { spawnSync } from "child_process";
|
|
5
20
|
import fs4 from "fs";
|
|
6
21
|
|
|
7
22
|
// node_modules/js-yaml/dist/js-yaml.mjs
|
|
@@ -3040,6 +3055,11 @@ var CHOMPING_STRIP = CHOMPING_MODE.STRIP;
|
|
|
3040
3055
|
var CHOMPING_KEEP = CHOMPING_MODE.KEEP;
|
|
3041
3056
|
|
|
3042
3057
|
// src/constants.ts
|
|
3058
|
+
import { createRequire } from "module";
|
|
3059
|
+
var require2 = createRequire(import.meta.url);
|
|
3060
|
+
var pkg = require2("../package.json");
|
|
3061
|
+
var VERSION = pkg.version;
|
|
3062
|
+
var PKG_NAME = pkg.name;
|
|
3043
3063
|
var AVAILABLE_MIRRORS = ["v6.gh-proxy.org", "gh-proxy.org", "hk.gh-proxy.org", "cdn.gh-proxy.org"];
|
|
3044
3064
|
var DEFAULT_MIRROR = "https://v6.gh-proxy.org/";
|
|
3045
3065
|
var UI_URLS = {
|
|
@@ -3171,6 +3191,43 @@ function rmrf(dir) {
|
|
|
3171
3191
|
// src/settings.ts
|
|
3172
3192
|
import fs2 from "fs";
|
|
3173
3193
|
import path2 from "path";
|
|
3194
|
+
|
|
3195
|
+
// src/errors.ts
|
|
3196
|
+
var TimeoutError = class extends Error {
|
|
3197
|
+
constructor() {
|
|
3198
|
+
super("timeout");
|
|
3199
|
+
this.name = "TimeoutError";
|
|
3200
|
+
}
|
|
3201
|
+
};
|
|
3202
|
+
var CliError = class extends Error {
|
|
3203
|
+
hint;
|
|
3204
|
+
label;
|
|
3205
|
+
exitCode;
|
|
3206
|
+
constructor(message, options = {}) {
|
|
3207
|
+
super(message);
|
|
3208
|
+
this.name = "CliError";
|
|
3209
|
+
this.label = options.label ?? "\u9519\u8BEF";
|
|
3210
|
+
this.hint = options.hint === void 0 ? [] : Array.isArray(options.hint) ? options.hint : [options.hint];
|
|
3211
|
+
this.exitCode = options.exitCode ?? 1;
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
3214
|
+
function withTimeout(promise, ms) {
|
|
3215
|
+
return new Promise((resolve, reject) => {
|
|
3216
|
+
const timer = setTimeout(() => reject(new TimeoutError()), ms);
|
|
3217
|
+
promise.then(
|
|
3218
|
+
(v) => {
|
|
3219
|
+
clearTimeout(timer);
|
|
3220
|
+
resolve(v);
|
|
3221
|
+
},
|
|
3222
|
+
(e) => {
|
|
3223
|
+
clearTimeout(timer);
|
|
3224
|
+
reject(e);
|
|
3225
|
+
}
|
|
3226
|
+
);
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// src/settings.ts
|
|
3174
3231
|
var settingsCache = null;
|
|
3175
3232
|
function readSettings() {
|
|
3176
3233
|
if (settingsCache !== null) return settingsCache;
|
|
@@ -3274,7 +3331,7 @@ function getSubscriptionsWithCache() {
|
|
|
3274
3331
|
var SAFE_NAME_RE = /^[\w\-\p{Unified_Ideograph}]{1,64}$/u;
|
|
3275
3332
|
function validateSubscriptionName(name) {
|
|
3276
3333
|
if (!name || !SAFE_NAME_RE.test(name)) {
|
|
3277
|
-
throw new
|
|
3334
|
+
throw new CliError(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${name}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C\u4E2D\u6587\uFF08\u6700\u957F 64 \u5B57\u7B26\uFF09`);
|
|
3278
3335
|
}
|
|
3279
3336
|
}
|
|
3280
3337
|
function addSubscription(url, name = "default") {
|
|
@@ -3282,7 +3339,7 @@ function addSubscription(url, name = "default") {
|
|
|
3282
3339
|
const settings = readSettings();
|
|
3283
3340
|
const subs = [...settings.subscriptions || []];
|
|
3284
3341
|
if (subs.some((s) => s.name === name)) {
|
|
3285
|
-
throw new
|
|
3342
|
+
throw new CliError(`\u8BA2\u9605 "${name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF08mihomo sub add <url> <\u540D\u79F0>\uFF09\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo sub remove ${name}\uFF09`);
|
|
3286
3343
|
}
|
|
3287
3344
|
subs.push({ name, url });
|
|
3288
3345
|
const updates = { subscriptions: subs };
|
|
@@ -3326,7 +3383,7 @@ function setDefaultSubscription(name) {
|
|
|
3326
3383
|
}
|
|
3327
3384
|
function getSubscriptionRawConfigPath(subName) {
|
|
3328
3385
|
if (!SAFE_NAME_RE.test(subName)) {
|
|
3329
|
-
throw new
|
|
3386
|
+
throw new CliError(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
|
|
3330
3387
|
}
|
|
3331
3388
|
return path2.join(DIRS.subscriptions, `${subName}.yaml`);
|
|
3332
3389
|
}
|
|
@@ -3564,26 +3621,7 @@ function listOverwriteFile() {
|
|
|
3564
3621
|
}
|
|
3565
3622
|
|
|
3566
3623
|
// src/utils.ts
|
|
3567
|
-
import { spawnSync } from "child_process";
|
|
3568
|
-
import { createRequire } from "module";
|
|
3569
|
-
var require2 = createRequire(import.meta.url);
|
|
3570
|
-
var pkg = require2("../package.json");
|
|
3571
|
-
var VERSION = pkg.version;
|
|
3572
|
-
var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
|
|
3573
3624
|
var sleepBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
3574
|
-
var NO_COLOR = process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
3575
|
-
function colorize(code, str) {
|
|
3576
|
-
if (NO_COLOR) return String(str);
|
|
3577
|
-
return `${code + String(str)}\x1B[0m`;
|
|
3578
|
-
}
|
|
3579
|
-
var colors = {
|
|
3580
|
-
bold: (s) => colorize("\x1B[1m", s),
|
|
3581
|
-
red: (s) => colorize("\x1B[31m", s),
|
|
3582
|
-
green: (s) => colorize("\x1B[32m", s),
|
|
3583
|
-
yellow: (s) => colorize("\x1B[33m", s),
|
|
3584
|
-
cyan: (s) => colorize("\x1B[36m", s),
|
|
3585
|
-
gray: (s) => colorize("\x1B[90m", s)
|
|
3586
|
-
};
|
|
3587
3625
|
function sleepSync(ms) {
|
|
3588
3626
|
Atomics.wait(sleepBuf, 0, 0, ms);
|
|
3589
3627
|
}
|
|
@@ -3596,39 +3634,6 @@ function escapeRegExp(s) {
|
|
|
3596
3634
|
function shellQuote(s) {
|
|
3597
3635
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
3598
3636
|
}
|
|
3599
|
-
var TimeoutError = class extends Error {
|
|
3600
|
-
constructor() {
|
|
3601
|
-
super("timeout");
|
|
3602
|
-
this.name = "TimeoutError";
|
|
3603
|
-
}
|
|
3604
|
-
};
|
|
3605
|
-
var CliError = class extends Error {
|
|
3606
|
-
hint;
|
|
3607
|
-
label;
|
|
3608
|
-
exitCode;
|
|
3609
|
-
constructor(message, options = {}) {
|
|
3610
|
-
super(message);
|
|
3611
|
-
this.name = "CliError";
|
|
3612
|
-
this.label = options.label ?? "\u9519\u8BEF";
|
|
3613
|
-
this.hint = options.hint === void 0 ? [] : Array.isArray(options.hint) ? options.hint : [options.hint];
|
|
3614
|
-
this.exitCode = options.exitCode ?? 1;
|
|
3615
|
-
}
|
|
3616
|
-
};
|
|
3617
|
-
function withTimeout(promise, ms) {
|
|
3618
|
-
return new Promise((resolve, reject) => {
|
|
3619
|
-
const timer = setTimeout(() => reject(new TimeoutError()), ms);
|
|
3620
|
-
promise.then(
|
|
3621
|
-
(v) => {
|
|
3622
|
-
clearTimeout(timer);
|
|
3623
|
-
resolve(v);
|
|
3624
|
-
},
|
|
3625
|
-
(e) => {
|
|
3626
|
-
clearTimeout(timer);
|
|
3627
|
-
reject(e);
|
|
3628
|
-
}
|
|
3629
|
-
);
|
|
3630
|
-
});
|
|
3631
|
-
}
|
|
3632
3637
|
function formatBytes(bytes) {
|
|
3633
3638
|
if (bytes === void 0 || bytes === null) return "\u672A\u77E5";
|
|
3634
3639
|
const num = Number(bytes);
|
|
@@ -3708,90 +3713,35 @@ function getNonFlagArg(args, startIdx, valueFlags = VALUE_FLAGS) {
|
|
|
3708
3713
|
}
|
|
3709
3714
|
return null;
|
|
3710
3715
|
}
|
|
3711
|
-
function
|
|
3712
|
-
if (
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
function isProcessCommandMatching(pid, needle) {
|
|
3721
|
-
if (!pid) return false;
|
|
3722
|
-
try {
|
|
3723
|
-
const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: 5e3 });
|
|
3724
|
-
return (result.stdout || "").includes(needle);
|
|
3725
|
-
} catch {
|
|
3726
|
-
return false;
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
function isProcessRoot(pid) {
|
|
3730
|
-
if (!pid) return false;
|
|
3731
|
-
try {
|
|
3732
|
-
const result = spawnSync("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: 5e3 });
|
|
3733
|
-
return (result.stdout || "").trim() === "0";
|
|
3734
|
-
} catch {
|
|
3735
|
-
return false;
|
|
3736
|
-
}
|
|
3737
|
-
}
|
|
3738
|
-
function createHttpClient(options = {}) {
|
|
3739
|
-
const { timeout = 6e4, secret } = options;
|
|
3740
|
-
const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
|
|
3741
|
-
return {
|
|
3742
|
-
async get(url, config) {
|
|
3743
|
-
const controller = new AbortController();
|
|
3744
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
3745
|
-
const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
|
|
3746
|
-
try {
|
|
3747
|
-
const response = await fetch(url, {
|
|
3748
|
-
signal,
|
|
3749
|
-
headers: { "User-Agent": `mihomo-cli/${VERSION}`, ...authHeaders }
|
|
3750
|
-
});
|
|
3751
|
-
if (!response.ok) {
|
|
3752
|
-
const error = new Error(`HTTP ${response.status}`);
|
|
3753
|
-
error.response = { status: response.status };
|
|
3754
|
-
try {
|
|
3755
|
-
error.response.data = await response.json();
|
|
3756
|
-
} catch {
|
|
3757
|
-
}
|
|
3758
|
-
throw error;
|
|
3759
|
-
}
|
|
3760
|
-
const declaredLen = Number(response.headers.get("content-length"));
|
|
3761
|
-
if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
|
|
3762
|
-
throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
|
|
3763
|
-
}
|
|
3764
|
-
const text = await readBodyWithLimit(response, controller);
|
|
3765
|
-
const data = config?.responseType === "json" ? JSON.parse(text) : text;
|
|
3766
|
-
return { data, headers: response.headers, status: response.status };
|
|
3767
|
-
} finally {
|
|
3768
|
-
clearTimeout(timer);
|
|
3769
|
-
}
|
|
3716
|
+
function levenshtein(a, b) {
|
|
3717
|
+
if (a === b) return 0;
|
|
3718
|
+
if (a.length === 0) return b.length;
|
|
3719
|
+
if (b.length === 0) return a.length;
|
|
3720
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
3721
|
+
for (let i = 1; i <= a.length; i++) {
|
|
3722
|
+
const curr = [i];
|
|
3723
|
+
for (let j = 1; j <= b.length; j++) {
|
|
3724
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
3770
3725
|
}
|
|
3771
|
-
|
|
3726
|
+
prev = curr;
|
|
3727
|
+
}
|
|
3728
|
+
return prev[b.length];
|
|
3772
3729
|
}
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
const
|
|
3776
|
-
const
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
if (total > MAX_RESPONSE_BYTES) {
|
|
3785
|
-
controller.abort();
|
|
3786
|
-
throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
|
|
3787
|
-
}
|
|
3788
|
-
chunks.push(value);
|
|
3789
|
-
}
|
|
3730
|
+
function suggestSimilar(input, candidates) {
|
|
3731
|
+
const lower = input.toLowerCase();
|
|
3732
|
+
const scored = [];
|
|
3733
|
+
for (const cand of candidates) {
|
|
3734
|
+
const c = cand.toLowerCase();
|
|
3735
|
+
if (cand === input) continue;
|
|
3736
|
+
if (c.startsWith(lower)) {
|
|
3737
|
+
scored.push({ name: cand, score: 0, lenDiff: Math.abs(cand.length - input.length) });
|
|
3738
|
+
} else if (lower.length >= 3) {
|
|
3739
|
+
const d = levenshtein(lower, c);
|
|
3740
|
+
if (d <= 2) scored.push({ name: cand, score: d, lenDiff: Math.abs(cand.length - input.length) });
|
|
3790
3741
|
}
|
|
3791
|
-
} finally {
|
|
3792
|
-
reader.releaseLock();
|
|
3793
3742
|
}
|
|
3794
|
-
|
|
3743
|
+
scored.sort((a, b) => a.score - b.score || a.lenDiff - b.lenDiff);
|
|
3744
|
+
return scored.slice(0, 3).map((s) => s.name);
|
|
3795
3745
|
}
|
|
3796
3746
|
function normalizeMirrorUrl(val) {
|
|
3797
3747
|
if (!val) return null;
|
|
@@ -3834,15 +3784,6 @@ function parseMirrorArg(args) {
|
|
|
3834
3784
|
}
|
|
3835
3785
|
return { mirror: null, isOverride: false, type: "download" };
|
|
3836
3786
|
}
|
|
3837
|
-
function isProxyValid(proxy) {
|
|
3838
|
-
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
3839
|
-
if (!proxy.type) return false;
|
|
3840
|
-
if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
|
|
3841
|
-
const pw = String(proxy.password || "");
|
|
3842
|
-
if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
|
|
3843
|
-
}
|
|
3844
|
-
return true;
|
|
3845
|
-
}
|
|
3846
3787
|
|
|
3847
3788
|
// src/config.ts
|
|
3848
3789
|
var SAFE_YAML_LOAD_OPTIONS = { maxAliases: 200 };
|
|
@@ -4097,7 +4038,7 @@ function getKernelVersion() {
|
|
|
4097
4038
|
}
|
|
4098
4039
|
if (kernelVersionCached) return kernelVersionCache;
|
|
4099
4040
|
try {
|
|
4100
|
-
const result =
|
|
4041
|
+
const result = spawnSync(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
|
|
4101
4042
|
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
4102
4043
|
if (output) {
|
|
4103
4044
|
const match = output.match(/v?[\d]+\.[\d]+\.[\d]+/);
|
|
@@ -4191,12 +4132,12 @@ function printVersion() {
|
|
|
4191
4132
|
}
|
|
4192
4133
|
|
|
4193
4134
|
// src/daemon.ts
|
|
4194
|
-
import { spawnSync as
|
|
4135
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4195
4136
|
import fs6 from "fs";
|
|
4196
4137
|
import path5 from "path";
|
|
4197
4138
|
|
|
4198
4139
|
// src/process.ts
|
|
4199
|
-
import { spawn, spawnSync as
|
|
4140
|
+
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
4200
4141
|
import fs5 from "fs";
|
|
4201
4142
|
import path4 from "path";
|
|
4202
4143
|
|
|
@@ -4233,6 +4174,34 @@ var SUDO_TIMEOUT_MS = 6e4;
|
|
|
4233
4174
|
var TUN_MODE_POST_WAIT_MS = 500;
|
|
4234
4175
|
var BATCH_KILL_THRESHOLD = 3;
|
|
4235
4176
|
var DEFAULT_LOG_RETENTION_DAYS = 7;
|
|
4177
|
+
var PS_TIMEOUT_MS = 5e3;
|
|
4178
|
+
function isProcessRunning(pid) {
|
|
4179
|
+
if (!pid) return false;
|
|
4180
|
+
try {
|
|
4181
|
+
const result = spawnSync2("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
|
|
4182
|
+
return (result.stdout || "").trim().length > 0;
|
|
4183
|
+
} catch {
|
|
4184
|
+
return false;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
function isProcessCommandMatching(pid, needle) {
|
|
4188
|
+
if (!pid) return false;
|
|
4189
|
+
try {
|
|
4190
|
+
const result = spawnSync2("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
|
|
4191
|
+
return (result.stdout || "").includes(needle);
|
|
4192
|
+
} catch {
|
|
4193
|
+
return false;
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
function isProcessRoot(pid) {
|
|
4197
|
+
if (!pid) return false;
|
|
4198
|
+
try {
|
|
4199
|
+
const result = spawnSync2("ps", ["-p", String(pid), "-o", "uid="], { encoding: "utf8", timeout: PS_TIMEOUT_MS });
|
|
4200
|
+
return (result.stdout || "").trim() === "0";
|
|
4201
|
+
} catch {
|
|
4202
|
+
return false;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4236
4205
|
var MAIN_INSTANCE_PATTERN = `${escapeRegExp(PATHS.mihomoBinary)}.*${escapeRegExp(PATHS.configFile)}`;
|
|
4237
4206
|
function clearRuntime() {
|
|
4238
4207
|
if (fs5.existsSync(DIRS.runtime)) {
|
|
@@ -4251,11 +4220,12 @@ function getPid() {
|
|
|
4251
4220
|
}
|
|
4252
4221
|
function isRunning() {
|
|
4253
4222
|
const pid = getPid();
|
|
4254
|
-
|
|
4223
|
+
if (!pid) return false;
|
|
4224
|
+
return isProcessRunning(pid) && isProcessCommandMatching(pid, PATHS.mihomoBinary);
|
|
4255
4225
|
}
|
|
4256
4226
|
function getMihomoPids() {
|
|
4257
4227
|
try {
|
|
4258
|
-
const result =
|
|
4228
|
+
const result = spawnSync2("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
|
|
4259
4229
|
const output = (result.stdout || "").trim();
|
|
4260
4230
|
if (!output) return [];
|
|
4261
4231
|
return output.split("\n").filter(Boolean).map((p) => parseInt(p, 10)).filter((p) => Number.isInteger(p) && p > 0);
|
|
@@ -4295,7 +4265,7 @@ function clearPid() {
|
|
|
4295
4265
|
if (!fs5.existsSync(PATHS.pidFile)) return;
|
|
4296
4266
|
if (isPidFileOwnedByRoot()) {
|
|
4297
4267
|
try {
|
|
4298
|
-
|
|
4268
|
+
spawnSync2("sudo", ["rm", "-f", PATHS.pidFile], { stdio: "inherit", timeout: 1e4 });
|
|
4299
4269
|
} catch {
|
|
4300
4270
|
}
|
|
4301
4271
|
} else {
|
|
@@ -4317,14 +4287,14 @@ function killAllMihomo(forceSudo = false) {
|
|
|
4317
4287
|
const pattern = MAIN_INSTANCE_PATTERN;
|
|
4318
4288
|
if (forceSudo) {
|
|
4319
4289
|
try {
|
|
4320
|
-
|
|
4290
|
+
spawnSync2("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
|
|
4321
4291
|
return true;
|
|
4322
4292
|
} catch {
|
|
4323
4293
|
return false;
|
|
4324
4294
|
}
|
|
4325
4295
|
} else {
|
|
4326
4296
|
try {
|
|
4327
|
-
|
|
4297
|
+
spawnSync2("pkill", ["-9", "-f", pattern], { timeout: 1e4 });
|
|
4328
4298
|
return true;
|
|
4329
4299
|
} catch {
|
|
4330
4300
|
return false;
|
|
@@ -4420,7 +4390,7 @@ exit 2
|
|
|
4420
4390
|
}
|
|
4421
4391
|
function getProcessInfo(pid) {
|
|
4422
4392
|
try {
|
|
4423
|
-
const result =
|
|
4393
|
+
const result = spawnSync2("ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8", timeout: 5e3 });
|
|
4424
4394
|
const psOutput = (result.stdout || "").trim();
|
|
4425
4395
|
if (!psOutput) return null;
|
|
4426
4396
|
const rss = parseInt(psOutput, 10);
|
|
@@ -4454,11 +4424,11 @@ async function start(mode = "mixed") {
|
|
|
4454
4424
|
rotateAndCleanupLogs();
|
|
4455
4425
|
const binary = PATHS.mihomoBinary;
|
|
4456
4426
|
if (!fs5.existsSync(binary)) {
|
|
4457
|
-
throw new
|
|
4427
|
+
throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
|
|
4458
4428
|
}
|
|
4459
4429
|
const configFile = PATHS.configFile;
|
|
4460
4430
|
if (!fs5.existsSync(configFile)) {
|
|
4461
|
-
throw new
|
|
4431
|
+
throw new CliError("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
|
|
4462
4432
|
}
|
|
4463
4433
|
const staleState = checkStaleState();
|
|
4464
4434
|
if (isTunMode) {
|
|
@@ -4535,7 +4505,7 @@ async function startTunMode(staleState) {
|
|
|
4535
4505
|
}
|
|
4536
4506
|
console.log("TUN \u6A21\u5F0F\u9700\u8981 sudo \u6743\u9650...");
|
|
4537
4507
|
try {
|
|
4538
|
-
const result =
|
|
4508
|
+
const result = spawnSync2("sudo", [launchScript], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
|
|
4539
4509
|
if (result.error) throw result.error;
|
|
4540
4510
|
if (result.status !== 0) {
|
|
4541
4511
|
const err = new Error("TUN \u542F\u52A8\u811A\u672C\u6267\u884C\u5931\u8D25");
|
|
@@ -4775,7 +4745,7 @@ function runSudoScript(scriptBody, opts) {
|
|
|
4775
4745
|
const scriptPath = path5.join(DIRS.runtime, opts.file);
|
|
4776
4746
|
fs6.writeFileSync(scriptPath, scriptBody, { mode: 448 });
|
|
4777
4747
|
try {
|
|
4778
|
-
const result =
|
|
4748
|
+
const result = spawnSync3("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
|
|
4779
4749
|
if (result.error) throw result.error;
|
|
4780
4750
|
if (result.status !== 0) {
|
|
4781
4751
|
if (result.status === 1) {
|
|
@@ -4809,10 +4779,10 @@ function isDaemonRunning(status) {
|
|
|
4809
4779
|
}
|
|
4810
4780
|
function enableDaemon() {
|
|
4811
4781
|
if (!fs6.existsSync(PATHS.mihomoBinary)) {
|
|
4812
|
-
throw new
|
|
4782
|
+
throw new CliError("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
|
|
4813
4783
|
}
|
|
4814
4784
|
if (!fs6.existsSync(PATHS.configFile)) {
|
|
4815
|
-
throw new
|
|
4785
|
+
throw new CliError("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
|
|
4816
4786
|
}
|
|
4817
4787
|
ensureDirs();
|
|
4818
4788
|
const stagePath = path5.join(DIRS.runtime, "daemon.plist.stage");
|
|
@@ -4917,6 +4887,67 @@ async function restartDaemon() {
|
|
|
4917
4887
|
cleanupOldLogs();
|
|
4918
4888
|
}
|
|
4919
4889
|
|
|
4890
|
+
// src/http.ts
|
|
4891
|
+
var MAX_RESPONSE_BYTES = 50 * 1024 * 1024;
|
|
4892
|
+
function createHttpClient(options = {}) {
|
|
4893
|
+
const { timeout = 6e4, secret } = options;
|
|
4894
|
+
const authHeaders = secret ? { Authorization: `Bearer ${secret}` } : {};
|
|
4895
|
+
return {
|
|
4896
|
+
async get(url, config) {
|
|
4897
|
+
const controller = new AbortController();
|
|
4898
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
4899
|
+
const signal = config?.signal ? AbortSignal.any([controller.signal, config.signal]) : controller.signal;
|
|
4900
|
+
try {
|
|
4901
|
+
const response = await fetch(url, {
|
|
4902
|
+
signal,
|
|
4903
|
+
headers: { "User-Agent": `mihomo-cli/${VERSION}`, ...authHeaders }
|
|
4904
|
+
});
|
|
4905
|
+
if (!response.ok) {
|
|
4906
|
+
const error = new Error(`HTTP ${response.status}`);
|
|
4907
|
+
error.response = { status: response.status };
|
|
4908
|
+
try {
|
|
4909
|
+
error.response.data = await response.json();
|
|
4910
|
+
} catch {
|
|
4911
|
+
}
|
|
4912
|
+
throw error;
|
|
4913
|
+
}
|
|
4914
|
+
const declaredLen = Number(response.headers.get("content-length"));
|
|
4915
|
+
if (Number.isFinite(declaredLen) && declaredLen > MAX_RESPONSE_BYTES) {
|
|
4916
|
+
throw new Error(`\u54CD\u5E94\u4F53\u8FC7\u5927\uFF08${formatBytes(declaredLen)}\uFF0C\u4E0A\u9650 ${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
|
|
4917
|
+
}
|
|
4918
|
+
const text = await readBodyWithLimit(response, controller);
|
|
4919
|
+
const data = config?.responseType === "json" ? JSON.parse(text) : text;
|
|
4920
|
+
return { data, headers: response.headers, status: response.status };
|
|
4921
|
+
} finally {
|
|
4922
|
+
clearTimeout(timer);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
async function readBodyWithLimit(response, controller) {
|
|
4928
|
+
if (!response.body) return response.text();
|
|
4929
|
+
const reader = response.body.getReader();
|
|
4930
|
+
const chunks = [];
|
|
4931
|
+
let total = 0;
|
|
4932
|
+
try {
|
|
4933
|
+
while (true) {
|
|
4934
|
+
const { done, value } = await reader.read();
|
|
4935
|
+
if (done) break;
|
|
4936
|
+
if (value) {
|
|
4937
|
+
total += value.byteLength;
|
|
4938
|
+
if (total > MAX_RESPONSE_BYTES) {
|
|
4939
|
+
controller.abort();
|
|
4940
|
+
throw new Error(`\u54CD\u5E94\u4F53\u8D85\u8FC7\u5927\u5C0F\u4E0A\u9650\uFF08${formatBytes(MAX_RESPONSE_BYTES)}\uFF09`);
|
|
4941
|
+
}
|
|
4942
|
+
chunks.push(value);
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
} finally {
|
|
4946
|
+
reader.releaseLock();
|
|
4947
|
+
}
|
|
4948
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4920
4951
|
// src/subscription.ts
|
|
4921
4952
|
function isGithubUrl(url) {
|
|
4922
4953
|
const githubRe = /github\.com|raw\.githubusercontent\.com/i;
|
|
@@ -4949,7 +4980,7 @@ function splitUrls(url) {
|
|
|
4949
4980
|
function loadSubscriptionConfig(subName) {
|
|
4950
4981
|
const rawContent = readSubscriptionRawConfig(subName);
|
|
4951
4982
|
if (!rawContent) {
|
|
4952
|
-
throw new
|
|
4983
|
+
throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u66F4\u65B0\u8BA2\u9605\uFF08mihomo sub update ${subName}\uFF09`);
|
|
4953
4984
|
}
|
|
4954
4985
|
const raw = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
|
|
4955
4986
|
return {
|
|
@@ -5165,7 +5196,7 @@ async function downloadMergedSubscription(urls, subName, signal, persist = true)
|
|
|
5165
5196
|
function prepareConfigForStart(mode, subName = "default") {
|
|
5166
5197
|
const rawContent = readSubscriptionRawConfig(subName);
|
|
5167
5198
|
if (!rawContent) {
|
|
5168
|
-
throw new
|
|
5199
|
+
throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
|
|
5169
5200
|
}
|
|
5170
5201
|
const subUrl = getSubscriptions().find((s) => s.name === subName)?.url;
|
|
5171
5202
|
const buildResult = buildConfig(rawContent, mode, { subName, subUrl });
|
|
@@ -5574,6 +5605,18 @@ function printStatus() {
|
|
|
5574
5605
|
subLine += ` (${formatProxySummary(info)})`;
|
|
5575
5606
|
}
|
|
5576
5607
|
console.log(subLine);
|
|
5608
|
+
const cached = getSubscriptionsWithCache().find((s) => s.name === activeSub.name);
|
|
5609
|
+
if (cached && (cached.download !== void 0 || cached.total !== void 0)) {
|
|
5610
|
+
const used = (cached.upload || 0) + (cached.download || 0);
|
|
5611
|
+
let trafficLine = `${colors.gray("\u6D41\u91CF: ")}${formatBytes(used)} / ${formatBytes(cached.total)}`;
|
|
5612
|
+
if (cached.total && cached.total > 0) {
|
|
5613
|
+
trafficLine += ` (${Math.min(used / cached.total * 100, 100).toFixed(1)}%)`;
|
|
5614
|
+
}
|
|
5615
|
+
console.log(trafficLine);
|
|
5616
|
+
}
|
|
5617
|
+
if (cached?.expire !== void 0) {
|
|
5618
|
+
console.log(`${colors.gray("\u5230\u671F: ")}${formatTimestamp(cached.expire)}`);
|
|
5619
|
+
}
|
|
5577
5620
|
} else {
|
|
5578
5621
|
console.log(`${colors.gray("\u8BA2\u9605: ")}\u672A\u914D\u7F6E`);
|
|
5579
5622
|
}
|
|
@@ -5615,10 +5658,14 @@ async function cmdStop() {
|
|
|
5615
5658
|
|
|
5616
5659
|
// src/commands/start.ts
|
|
5617
5660
|
async function cmdStart(args) {
|
|
5661
|
+
const modeToken = args[1] && !args[1].startsWith("-") ? args[1].toLowerCase() : void 0;
|
|
5662
|
+
if (modeToken !== void 0 && modeToken !== "tun" && modeToken !== "mixed") {
|
|
5663
|
+
throw new CliError(`\u672A\u77E5\u7684\u542F\u52A8\u6A21\u5F0F: ${args[1]}`, { hint: "\u7528\u6CD5: mihomo start [tun|mixed]\uFF08\u9ED8\u8BA4 mixed\uFF09" });
|
|
5664
|
+
}
|
|
5665
|
+
const targetMode = modeToken === "tun" ? "tun" : "mixed";
|
|
5618
5666
|
if (!hasKernel()) {
|
|
5619
5667
|
throw new CliError('\u672A\u627E\u5230\u5185\u6838\uFF0C\u8BF7\u8FD0\u884C "mihomo kernel"');
|
|
5620
5668
|
}
|
|
5621
|
-
const targetMode = args[1] === "tun" ? "tun" : "mixed";
|
|
5622
5669
|
const daemonEnabled = isDaemonEnabled();
|
|
5623
5670
|
if (targetMode === "tun" && daemonEnabled) {
|
|
5624
5671
|
throw new CliError("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF08\u4EC5\u652F\u6301 Mixed \u6A21\u5F0F\uFF09\uFF0C\u65E0\u6CD5\u542F\u52A8 TUN", { hint: "\u8BF7\u5148\u5173\u95ED\u4FDD\u6D3B: mihomo daemon off" });
|
|
@@ -5812,7 +5859,11 @@ async function cmdDaemon(args) {
|
|
|
5812
5859
|
// 无 action → 显示状态;未知 action → 报错
|
|
5813
5860
|
fallback: printStatusView,
|
|
5814
5861
|
onUnknown: (action) => {
|
|
5815
|
-
|
|
5862
|
+
const names = SUBCOMMANDS.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
5863
|
+
const suggestion = suggestSimilar(action, names);
|
|
5864
|
+
throw new CliError(`\u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`, {
|
|
5865
|
+
hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status"]
|
|
5866
|
+
});
|
|
5816
5867
|
}
|
|
5817
5868
|
});
|
|
5818
5869
|
}
|
|
@@ -5882,7 +5933,7 @@ function cmdDirectory(args) {
|
|
|
5882
5933
|
}
|
|
5883
5934
|
|
|
5884
5935
|
// src/kernel.ts
|
|
5885
|
-
import { spawnSync as
|
|
5936
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
5886
5937
|
import fs7 from "fs";
|
|
5887
5938
|
import path6 from "path";
|
|
5888
5939
|
|
|
@@ -6047,7 +6098,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6047
6098
|
if (progressCallback) {
|
|
6048
6099
|
progressCallback(`\u4E0B\u8F7D\u5185\u6838: ${asset.name} (${sizeMB} MB)`);
|
|
6049
6100
|
}
|
|
6050
|
-
const curlResult =
|
|
6101
|
+
const curlResult = spawnSync4(
|
|
6051
6102
|
"curl",
|
|
6052
6103
|
["-L", "--progress-bar", "--connect-timeout", "30", "--max-time", String(Math.floor(KERNEL_DOWNLOAD_TIMEOUT / 1e3)), "-o", tempPath, downloadUrl],
|
|
6053
6104
|
{ stdio: "inherit" }
|
|
@@ -6075,7 +6126,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6075
6126
|
let extractedBinary = null;
|
|
6076
6127
|
try {
|
|
6077
6128
|
if (tempPath.endsWith(".tar.gz") || tempPath.endsWith(".tgz")) {
|
|
6078
|
-
const listResult =
|
|
6129
|
+
const listResult = spawnSync4("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
|
|
6079
6130
|
if (listResult.error) throw listResult.error;
|
|
6080
6131
|
if (listResult.status !== 0) throw new Error(`tar \u5217\u8868\u9000\u51FA\u7801 ${listResult.status}`);
|
|
6081
6132
|
const entries = (listResult.stdout || "").split("\n").filter(Boolean);
|
|
@@ -6084,13 +6135,13 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6084
6135
|
throw new Error(`\u5F52\u6863\u542B\u975E\u6CD5\u8DEF\u5F84\u6761\u76EE: ${entry}`);
|
|
6085
6136
|
}
|
|
6086
6137
|
}
|
|
6087
|
-
const tarResult =
|
|
6138
|
+
const tarResult = spawnSync4("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
|
|
6088
6139
|
if (tarResult.error) throw tarResult.error;
|
|
6089
6140
|
if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
|
|
6090
6141
|
} else if (tempPath.endsWith(".gz")) {
|
|
6091
6142
|
const baseName = path6.basename(tempPath, ".gz");
|
|
6092
6143
|
const outputPath = path6.join(extractPath, baseName);
|
|
6093
|
-
const gzipResult =
|
|
6144
|
+
const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
|
|
6094
6145
|
if (gzipResult.error) throw gzipResult.error;
|
|
6095
6146
|
if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
|
|
6096
6147
|
fs7.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
|
|
@@ -6126,7 +6177,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6126
6177
|
if (progressCallback) {
|
|
6127
6178
|
progressCallback("\u6821\u9A8C\u5185\u6838...");
|
|
6128
6179
|
}
|
|
6129
|
-
const check =
|
|
6180
|
+
const check = spawnSync4(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
|
|
6130
6181
|
const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
|
|
6131
6182
|
if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
|
|
6132
6183
|
try {
|
|
@@ -6171,6 +6222,9 @@ async function cmdKernel(args) {
|
|
|
6171
6222
|
const result = await downloadKernel((msg) => console.log(msg), mirrorInfo.mirror, info.release);
|
|
6172
6223
|
console.log(`
|
|
6173
6224
|
\u5DF2\u66F4\u65B0\u5230 ${result.version}`);
|
|
6225
|
+
if (getRunningState().running) {
|
|
6226
|
+
console.log(colors.yellow("\u63D0\u793A: \u8FD0\u884C\u4E2D\u7684\u5185\u6838\u4ECD\u662F\u65E7\u7248\u672C\uFF0C\u6267\u884C mihomo start \u91CD\u542F\u540E\u751F\u6548"));
|
|
6227
|
+
}
|
|
6174
6228
|
}
|
|
6175
6229
|
} catch (e) {
|
|
6176
6230
|
if (e instanceof CliError) throw e;
|
|
@@ -6530,6 +6584,15 @@ async function cmdReset(args) {
|
|
|
6530
6584
|
import { spawn as spawn2 } from "child_process";
|
|
6531
6585
|
import fs9 from "fs";
|
|
6532
6586
|
import path8 from "path";
|
|
6587
|
+
function isProxyValid(proxy) {
|
|
6588
|
+
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
6589
|
+
if (!proxy.type) return false;
|
|
6590
|
+
if (proxy.type === "ss" && typeof proxy.cipher === "string" && proxy.cipher.startsWith("2022-blake3")) {
|
|
6591
|
+
const pw = String(proxy.password || "");
|
|
6592
|
+
if (!/^[A-Za-z0-9+/\-_]+=*$/.test(pw) || pw.length < 20) return false;
|
|
6593
|
+
}
|
|
6594
|
+
return true;
|
|
6595
|
+
}
|
|
6533
6596
|
var TEST_DIR = path8.join(USER_DATA_DIR, "test");
|
|
6534
6597
|
var TEST_DIRS = {
|
|
6535
6598
|
data: path8.join(TEST_DIR, "data"),
|
|
@@ -6553,12 +6616,12 @@ function buildTestConfig(subName) {
|
|
|
6553
6616
|
ensureTestDirs();
|
|
6554
6617
|
const rawContent = readSubscriptionRawConfig(subName);
|
|
6555
6618
|
if (!rawContent) {
|
|
6556
|
-
throw new
|
|
6619
|
+
throw new CliError(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u66F4\u65B0\u8BA2\u9605\uFF08mihomo sub update ${subName}\uFF09`);
|
|
6557
6620
|
}
|
|
6558
6621
|
const parsed = parseYamlOrJson(rawContent, "\u8BA2\u9605\u5185\u5BB9");
|
|
6559
6622
|
const proxies = (parsed.proxies || []).filter(isProxyValid);
|
|
6560
6623
|
if (proxies.length === 0) {
|
|
6561
|
-
throw new
|
|
6624
|
+
throw new CliError(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
|
|
6562
6625
|
}
|
|
6563
6626
|
const nameCount = /* @__PURE__ */ new Map();
|
|
6564
6627
|
for (const proxy of proxies) {
|
|
@@ -6585,7 +6648,7 @@ function buildTestConfig(subName) {
|
|
|
6585
6648
|
}
|
|
6586
6649
|
async function startTestInstance() {
|
|
6587
6650
|
const binary = PATHS.mihomoBinary;
|
|
6588
|
-
if (!fs9.existsSync(binary)) throw new
|
|
6651
|
+
if (!fs9.existsSync(binary)) throw new CliError('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
|
|
6589
6652
|
stopTestInstance();
|
|
6590
6653
|
const logFd = fs9.openSync(TEST_PATHS.logFile, "a");
|
|
6591
6654
|
const child = spawn2(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
|
|
@@ -6597,7 +6660,7 @@ async function startTestInstance() {
|
|
|
6597
6660
|
fs9.closeSync(logFd);
|
|
6598
6661
|
child.unref();
|
|
6599
6662
|
const pid = child.pid;
|
|
6600
|
-
if (!pid) throw new
|
|
6663
|
+
if (!pid) throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25\uFF1A\u65E0\u6CD5\u521B\u5EFA\u8FDB\u7A0B\uFF08\u5185\u6838\u4E8C\u8FDB\u5236\u53EF\u80FD\u4E0D\u53EF\u6267\u884C\uFF09");
|
|
6601
6664
|
fs9.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
|
|
6602
6665
|
const client = createHttpClient({ timeout: 2e3 });
|
|
6603
6666
|
let ready = false;
|
|
@@ -6617,11 +6680,11 @@ async function startTestInstance() {
|
|
|
6617
6680
|
errorDetail = fs9.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
|
|
6618
6681
|
} catch {
|
|
6619
6682
|
}
|
|
6620
|
-
throw new
|
|
6683
|
+
throw new CliError(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
|
|
6621
6684
|
${errorDetail}` : ""}`);
|
|
6622
6685
|
}
|
|
6623
6686
|
if (!ready) {
|
|
6624
|
-
throw new
|
|
6687
|
+
throw new CliError("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
|
|
6625
6688
|
}
|
|
6626
6689
|
}
|
|
6627
6690
|
function stopTestInstance() {
|
|
@@ -6759,8 +6822,8 @@ async function subAdd(args) {
|
|
|
6759
6822
|
}
|
|
6760
6823
|
const normalizedUrl = urls.join(",");
|
|
6761
6824
|
console.log(`\u6DFB\u52A0\u5408\u5E76\u8BA2\u9605: ${name} (${urls.length} \u4E2A\u6E90)`);
|
|
6825
|
+
addSubscription(normalizedUrl, name);
|
|
6762
6826
|
try {
|
|
6763
|
-
addSubscription(normalizedUrl, name);
|
|
6764
6827
|
setDefaultSubscription(name);
|
|
6765
6828
|
const info = await downloadMergedSubscription(urls, name);
|
|
6766
6829
|
console.log(`\u5DF2\u6DFB\u52A0\u5E76\u5207\u6362\u5230 "${name}" (${formatProxySummary(info)}, \u5408\u5E76 ${urls.length} \u6E90)`);
|
|
@@ -6773,8 +6836,8 @@ async function subAdd(args) {
|
|
|
6773
6836
|
throw new CliError("\u8BF7\u63D0\u4F9B\u6709\u6548\u7684\u8BA2\u9605 URL\uFF08\u9700\u4EE5 http:// \u6216 https:// \u5F00\u5934\uFF09");
|
|
6774
6837
|
}
|
|
6775
6838
|
console.log(`\u6DFB\u52A0\u8BA2\u9605: ${name}`);
|
|
6839
|
+
addSubscription(url, name);
|
|
6776
6840
|
try {
|
|
6777
|
-
addSubscription(url, name);
|
|
6778
6841
|
setDefaultSubscription(name);
|
|
6779
6842
|
const info = await downloadSubscription(url, name);
|
|
6780
6843
|
const repoUrl = githubRepoUrl(url);
|
|
@@ -6956,8 +7019,12 @@ async function cmdSubscription(args) {
|
|
|
6956
7019
|
await dispatchSubcommand(args, SUBCOMMANDS4, {
|
|
6957
7020
|
// 无子命令 → 列表;未知子命令 → 报错
|
|
6958
7021
|
fallback: printSubscriptionList,
|
|
6959
|
-
onUnknown: () => {
|
|
6960
|
-
|
|
7022
|
+
onUnknown: (action) => {
|
|
7023
|
+
const names = SUBCOMMANDS4.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
7024
|
+
const suggestion = suggestSimilar(action, names);
|
|
7025
|
+
throw new CliError(`\u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4: ${action}`, {
|
|
7026
|
+
hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]"]
|
|
7027
|
+
});
|
|
6961
7028
|
}
|
|
6962
7029
|
});
|
|
6963
7030
|
}
|
|
@@ -7034,6 +7101,10 @@ function cmdUI(args) {
|
|
|
7034
7101
|
if (!Object.hasOwn(UI_URLS, uiName)) {
|
|
7035
7102
|
throw new CliError(`\u672A\u77E5\u7684 UI "${uiName}"`, { hint: "\u53EF\u7528 UI: zash (\u9ED8\u8BA4), dash, yacd" });
|
|
7036
7103
|
}
|
|
7104
|
+
if (!getRunningState().running) {
|
|
7105
|
+
console.log(colors.yellow("\u63D0\u793A: mihomo \u672A\u8FD0\u884C\uFF0CUI \u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5\uFF08\u5148\u6267\u884C mihomo start \u542F\u52A8\uFF09"));
|
|
7106
|
+
console.log("");
|
|
7107
|
+
}
|
|
7037
7108
|
const url = UI_URLS[uiName];
|
|
7038
7109
|
console.log(`\u6253\u5F00 Web UI: ${uiName}`);
|
|
7039
7110
|
console.log(`\u5730\u5740: ${url}`);
|
|
@@ -7048,16 +7119,38 @@ function cmdUI(args) {
|
|
|
7048
7119
|
}
|
|
7049
7120
|
|
|
7050
7121
|
// src/commands/update.ts
|
|
7051
|
-
import {
|
|
7122
|
+
import { execFile, spawn as spawn3 } from "child_process";
|
|
7052
7123
|
import { promisify } from "util";
|
|
7053
|
-
var
|
|
7124
|
+
var execFileAsync = promisify(execFile);
|
|
7125
|
+
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
7126
|
+
async function getLatestNpmVersion() {
|
|
7127
|
+
try {
|
|
7128
|
+
const { stdout } = await execFileAsync("npm", ["view", PKG_NAME, "version"], { timeout: NPM_VIEW_TIMEOUT_MS });
|
|
7129
|
+
const version = stdout.trim().split("\n").pop()?.trim();
|
|
7130
|
+
return version || null;
|
|
7131
|
+
} catch {
|
|
7132
|
+
return null;
|
|
7133
|
+
}
|
|
7134
|
+
}
|
|
7054
7135
|
async function cmdUpdate() {
|
|
7055
7136
|
console.log(`\u5F53\u524D\u7248\u672C: ${colors.cyan(VERSION)}`);
|
|
7056
7137
|
console.log("");
|
|
7138
|
+
console.log("\u6B63\u5728\u68C0\u67E5\u6700\u65B0\u7248\u672C...");
|
|
7139
|
+
const latest = await getLatestNpmVersion();
|
|
7140
|
+
if (latest && latest === VERSION) {
|
|
7141
|
+
console.log(`\u5DF2\u662F\u6700\u65B0\u7248\u672C (${colors.green(VERSION)})\uFF0C\u65E0\u9700\u66F4\u65B0`);
|
|
7142
|
+
return;
|
|
7143
|
+
}
|
|
7144
|
+
if (latest) {
|
|
7145
|
+
console.log(`\u6700\u65B0\u7248\u672C: ${colors.cyan(latest)}`);
|
|
7146
|
+
} else {
|
|
7147
|
+
console.log(colors.yellow("\u65E0\u6CD5\u67E5\u8BE2\u6700\u65B0\u7248\u672C\uFF08\u7F51\u7EDC\u95EE\u9898\uFF1F\uFF09\uFF0C\u5C06\u76F4\u63A5\u5C1D\u8BD5\u91CD\u65B0\u5B89\u88C5"));
|
|
7148
|
+
}
|
|
7149
|
+
console.log("");
|
|
7057
7150
|
console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
|
|
7058
7151
|
console.log("");
|
|
7059
7152
|
await new Promise((resolve, reject) => {
|
|
7060
|
-
const npm = spawn3("npm", ["install", "-g",
|
|
7153
|
+
const npm = spawn3("npm", ["install", "-g", PKG_NAME], { stdio: "inherit" });
|
|
7061
7154
|
npm.on("close", (code) => {
|
|
7062
7155
|
if (code === 0) {
|
|
7063
7156
|
resolve();
|
|
@@ -7071,9 +7164,9 @@ async function cmdUpdate() {
|
|
|
7071
7164
|
});
|
|
7072
7165
|
});
|
|
7073
7166
|
try {
|
|
7074
|
-
const { stdout } = await
|
|
7167
|
+
const { stdout } = await execFileAsync("npm", ["list", "-g", PKG_NAME, "--json", "--depth=0"]);
|
|
7075
7168
|
const result = JSON.parse(stdout);
|
|
7076
|
-
const newVersion = result.dependencies?.[
|
|
7169
|
+
const newVersion = result.dependencies?.[PKG_NAME]?.version;
|
|
7077
7170
|
console.log("");
|
|
7078
7171
|
if (newVersion) {
|
|
7079
7172
|
console.log(`\u66F4\u65B0\u5B8C\u6210\uFF0C\u6700\u65B0\u7248\u672C: ${colors.green(newVersion)}`);
|
|
@@ -7278,6 +7371,9 @@ var COMMAND_INDEX = (() => {
|
|
|
7278
7371
|
function findCommand(token) {
|
|
7279
7372
|
return COMMAND_INDEX.get(token);
|
|
7280
7373
|
}
|
|
7374
|
+
function allCommandTokens() {
|
|
7375
|
+
return [...COMMAND_INDEX.keys()];
|
|
7376
|
+
}
|
|
7281
7377
|
|
|
7282
7378
|
// src/index.ts
|
|
7283
7379
|
process.on("SIGINT", () => {
|
|
@@ -7327,7 +7423,10 @@ async function main() {
|
|
|
7327
7423
|
const token = args[0].toLowerCase();
|
|
7328
7424
|
const command = findCommand(token);
|
|
7329
7425
|
if (!command) {
|
|
7330
|
-
|
|
7426
|
+
const suggestion = suggestSimilar(token, allCommandTokens());
|
|
7427
|
+
throw new CliError(`\u672A\u77E5\u547D\u4EE4: ${token}`, {
|
|
7428
|
+
hint: [suggestion.length > 0 ? `\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?` : '\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9']
|
|
7429
|
+
});
|
|
7331
7430
|
}
|
|
7332
7431
|
await command.handler(command.rewrite ? command.rewrite(args) : args);
|
|
7333
7432
|
}
|