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 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 (npm install -g) |
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 as spawnSync2 } from "child_process";
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 Error(`\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`);
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 Error(`\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`);
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 Error(`\u8BA2\u9605\u540D\u79F0\u65E0\u6548: "${subName}"`);
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 isProcessRunning(pid) {
3712
- if (!pid) return false;
3713
- try {
3714
- const result = spawnSync("ps", ["-p", String(pid), "-o", "pid="], { encoding: "utf8", timeout: 5e3 });
3715
- return (result.stdout || "").trim().length > 0;
3716
- } catch {
3717
- return false;
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
- async function readBodyWithLimit(response, controller) {
3774
- if (!response.body) return response.text();
3775
- const reader = response.body.getReader();
3776
- const chunks = [];
3777
- let total = 0;
3778
- try {
3779
- while (true) {
3780
- const { done, value } = await reader.read();
3781
- if (done) break;
3782
- if (value) {
3783
- total += value.byteLength;
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
- return Buffer.concat(chunks).toString("utf8");
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 = spawnSync2(PATHS.mihomoBinary, ["-v"], { encoding: "utf8", timeout: 5e3 });
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 spawnSync4 } from "child_process";
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 spawnSync3 } from "child_process";
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
- return pid ? isProcessRunning(pid) : false;
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 = spawnSync3("pgrep", ["-f", MAIN_INSTANCE_PATTERN], { encoding: "utf8", timeout: 1e4 });
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
- spawnSync3("sudo", ["rm", "-f", PATHS.pidFile], { stdio: "inherit", timeout: 1e4 });
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
- spawnSync3("sudo", ["pkill", "-9", "-f", pattern], { stdio: "inherit", timeout: 15e3 });
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
- spawnSync3("pkill", ["-9", "-f", pattern], { timeout: 1e4 });
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 = spawnSync3("ps", ["-p", String(pid), "-o", "rss="], { encoding: "utf8", timeout: 5e3 });
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 Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
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 Error("\u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605\u5E76\u542F\u52A8");
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 = spawnSync3("sudo", [launchScript], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
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 = spawnSync4("sudo", [scriptPath], { stdio: "inherit", timeout: SUDO_TIMEOUT_MS });
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 Error("\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u4E0B\u8F7D\u5185\u6838");
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 Error("\u672A\u627E\u5230\u8FD0\u884C\u65F6\u914D\u7F6E\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
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 Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
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 Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605`);
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
- throw new CliError(`\u672A\u77E5\u7684 daemon \u5B50\u547D\u4EE4: ${action}`, { hint: ["", "\u53EF\u7528\u5B50\u547D\u4EE4: on, off, status"] });
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 spawnSync5 } from "child_process";
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 = spawnSync5(
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 = spawnSync5("tar", ["-tzf", tempPath], { encoding: "utf8", timeout: 6e4 });
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 = spawnSync5("tar", ["-xzf", tempPath, "-C", extractPath], { stdio: ["ignore", "ignore", "inherit"], timeout: 6e4 });
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 = spawnSync5("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
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 = spawnSync5(targetPath, ["-v"], { encoding: "utf8", timeout: 5e3 });
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 Error(`\u672A\u627E\u5230\u8BA2\u9605\u914D\u7F6E "${subName}"`);
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 Error(`\u8BA2\u9605 "${subName}" \u6CA1\u6709\u6709\u6548\u8282\u70B9`);
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 Error('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
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 Error("\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");
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 Error(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
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 Error("\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u8D85\u65F6\uFF0CAPI \u672A\u54CD\u5E94");
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
- throw new CliError("\u672A\u77E5\u7684\u8BA2\u9605\u547D\u4EE4", { hint: "\u7528\u6CD5: mihomo sub [list|use|add|update|remove|web|test|clean]" });
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 { exec, spawn as spawn3 } from "child_process";
7122
+ import { execFile, spawn as spawn3 } from "child_process";
7052
7123
  import { promisify } from "util";
7053
- var execAsync = promisify(exec);
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", "mihomo-cli"], { stdio: "inherit" });
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 execAsync("npm list -g mihomo-cli --json --depth=0");
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?.["mihomo-cli"]?.version;
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
- throw new CliError(`\u672A\u77E5\u547D\u4EE4: ${token}`, { hint: '\u4F7F\u7528 "mihomo help" \u67E5\u770B\u5E2E\u52A9' });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mihomo-cli",
3
- "version": "3.5.0",
3
+ "version": "3.6.0",
4
4
  "type": "module",
5
5
  "description": "A terminal-based mihomo (Clash.Meta) client for macOS",
6
6
  "bin": {