mihomo-cli 3.7.0 → 3.8.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 +28 -0
- package/README.md +61 -3
- package/dist/index.js +749 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.8.0] - 2026-09-01
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- **ssh 隧道出口(`tunnel` 子命令,别名 `ssh`)** - 管理 `ssh -D` 动态转发进程的生命周期,把可 ssh 登录的机器变成本地 SOCKS5 出口。分流仍交给覆写机制,本功能补的是「隧道断了 mihomo 不知情、会一直往死端口送流量」这一环
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
mihomo tunnel add work --host m4 --port 1080
|
|
11
|
+
mihomo tunnel up|down|status [名字]
|
|
12
|
+
mihomo tunnel rm <名字>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **随 `start` 一并拉起**:默认带 `auto` 标记,`start` 顺带启动、`stop` 连带停止(`--no-tunnel` 跳过,`add --no-auto` 不参与)
|
|
16
|
+
- **隧道失败不影响内核启动**:只影响内网分流那部分规则,故仅打印显眼的黄色警告并附上 ssh 给出的原因,其余流量照常
|
|
17
|
+
- **`stop` 只停自己起的**:手动 `tunnel up` 起的隧道带 `manual` 标记,不会被 `mihomo stop` 带走,避免下次 start 又起一个而累积僵尸进程
|
|
18
|
+
- **`status` 真实探测端口**:能识别「进程还在但转发已死」的假活——那正是最误导的形态,此时 mihomo 仍在往死端口送流量
|
|
19
|
+
- **起之前先检测端口占用**,不盲启后失败
|
|
20
|
+
- **`add` 生成覆写模板** `overwrite.tunnel-<名字>.yaml`(已建好 socks5 节点与 select 分组,分流规则留白待填),生成后完全由用户维护,CLI 不再改写,`tunnel rm` 也不删它
|
|
21
|
+
- 新增 `reset tunnel` 目标(先停进程再删运行态,反序会导致 ssh 进程失联且再也停不掉)
|
|
22
|
+
|
|
23
|
+
安全边界:`-D` 恒绑 `127.0.0.1` 且不提供绑定地址开关(绑 `0.0.0.0` 会让同一 WiFi 下任何设备经本机进内网);`--host` 拒绝 `-` 开头的值(`-oProxyCommand=...` 等同任意命令执行);ssh 参数固定带 `ExitOnForwardFailure`/`BatchMode`/`ConnectTimeout`/`ServerAlive*`
|
|
24
|
+
|
|
25
|
+
暂不做自动重连保活:断线依靠 `ServerAliveInterval` 让进程自退,再用 `tunnel status` 查出来
|
|
26
|
+
|
|
27
|
+
### 修复
|
|
28
|
+
|
|
29
|
+
- **`reset --full` 会留下空的 `settings.json`** - 新增的 `tunnel` 目标其 `onAfter` 会 `writeSettings` 清空隧道列表,若排在 `settings` 之后就会把刚删掉的文件重建成 `{}`,与「已重置: 设置」矛盾。现移到 `settings` 之前(与 `subs` 同理),并加单测锁定该顺序约束——这是 v3.7.0 已修过一次的同类问题(当时是参数顺序),换个目标又复发了
|
|
30
|
+
|
|
3
31
|
## [3.7.0] - 2026-08-22
|
|
4
32
|
|
|
5
33
|
### 修复
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- 🔄 **智能重启** - `sub use` 切换订阅、`ow on/off` 切换覆写后自动重启
|
|
15
15
|
- 🚀 **进程管理** - 启动/停止/切换模式,自动清理残留进程
|
|
16
16
|
- 🛡️ **进程保活** - 基于 launchd(root),崩溃/开机自动拉起,代理后台常驻(`daemon on`)
|
|
17
|
+
- 🔌 **ssh 隧道出口** - 管理 `ssh -D` 进程生命周期,把内网机器变成本地 SOCKS5 出口,随 `start` 一并拉起
|
|
17
18
|
- 🔄 **双模式支持** - Mixed 模式和 TUN 透明代理模式
|
|
18
19
|
- 📊 **状态监控** - 查看运行状态、内存占用、订阅流量与到期时间
|
|
19
20
|
- 📝 **日志管理** - 实时日志 + 历史日志归档(自动轮转,保留7天)
|
|
@@ -126,11 +127,16 @@ mihomo ui yacd # YACD
|
|
|
126
127
|
| --------------------------------- | ------------------------------------------------------------------- |
|
|
127
128
|
| `mihomo kernel [--mirror [镜像]]` | 更新内核(默认直连,`--mirror` 使用镜像;更新后运行中实例需重启生效) |
|
|
128
129
|
| `mihomo daemon [on\|off\|status]` | 进程保活:开机自启 + 崩溃自动重启(仅 Mixed 模式,on/off 需管理员密码) |
|
|
130
|
+
| `mihomo tunnel` | 列出 ssh 隧道及真实状态(别名 `ssh`/`tunnels`) |
|
|
131
|
+
| `mihomo tunnel add <名字> --host <主机> --port <端口> [--no-auto]` | 添加隧道并生成覆写模板(默认随 start 拉起) |
|
|
132
|
+
| `mihomo tunnel up\|down [名字]` | 启动/停止隧道(无参即全部) |
|
|
133
|
+
| `mihomo tunnel status [名字]` | 查看隧道状态(真实探测端口,能识别「假活」) |
|
|
134
|
+
| `mihomo tunnel rm <名字> [-y]` | 删除隧道(不删覆写文件) |
|
|
129
135
|
| `mihomo update` | 更新 mihomo-cli(先查 npm 最新版,已是最新则跳过重装) |
|
|
130
136
|
| `mihomo ui [zash\|dash\|yacd]` | 打开 Web UI |
|
|
131
137
|
| `mihomo dir` | 显示数据目录位置 |
|
|
132
138
|
| `mihomo dir open [target]` | 打开指定目录(`root`, `subs`, `logs`, `data`, `runtime`, `kernel`) |
|
|
133
|
-
| `mihomo reset [目标...] [--full] [-y]` | 重置用户数据(可用目标:`subs`, `logs`, `data`, `runtime`, `settings`, `kernel`, `overwrites`, `daemon`;`--full` 删全部,`-y` 跳过确认) |
|
|
139
|
+
| `mihomo reset [目标...] [--full] [-y]` | 重置用户数据(可用目标:`subs`, `logs`, `data`, `runtime`, `settings`, `kernel`, `overwrites`, `daemon`, `tunnel`;`--full` 删全部,`-y` 跳过确认) |
|
|
134
140
|
| `mihomo version` | 显示版本信息 |
|
|
135
141
|
| `mihomo help` | 显示帮助信息 |
|
|
136
142
|
|
|
@@ -143,7 +149,7 @@ mihomo ui yacd # YACD
|
|
|
143
149
|
- `mhm`
|
|
144
150
|
- `mh`
|
|
145
151
|
|
|
146
|
-
子命令组亦有别名:`subscription` = `sub`/`subs`/`subscriptions`,`directory` = `dir`/`dirs`/`directories`,`overwrite` = `ow`
|
|
152
|
+
子命令组亦有别名:`subscription` = `sub`/`subs`/`subscriptions`,`directory` = `dir`/`dirs`/`directories`,`overwrite` = `ow`,`tunnel` = `ssh`/`tunnels`(`tun` 已被 TUN 模式占用)
|
|
147
153
|
|
|
148
154
|
### 快捷命令
|
|
149
155
|
|
|
@@ -173,6 +179,54 @@ mihomo ui yacd # YACD
|
|
|
173
179
|
- 需要 sudo / 管理员权限
|
|
174
180
|
- 首次使用会自动配置 DNS 和路由
|
|
175
181
|
|
|
182
|
+
## ssh 隧道出口
|
|
183
|
+
|
|
184
|
+
把一台可 ssh 登录的机器(如公司内网的机器)变成本地 SOCKS5 出口,配合覆写规则即可只让内网域名走它,其余流量照常走订阅节点。本功能负责的是 **ssh 进程的生命周期管理**——隧道断了 mihomo 并不知情,会一直往死端口送流量。
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
mihomo tunnel add work --host m4 --port 1080 # m4 是 ~/.ssh/config 里的别名
|
|
188
|
+
mihomo tunnel up work # 启动
|
|
189
|
+
mihomo tunnel status # 查看状态(真实探测端口)
|
|
190
|
+
mihomo tunnel down work # 停止
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`add` 会生成一份覆写模板 `overwrite.tunnel-<名字>.yaml`,其中已建好 socks5 节点与分组,
|
|
194
|
+
**分流规则留白待你填写**(CLI 无从知道你的内网域名):
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
# 取消注释并改成你的内网域名/网段
|
|
198
|
+
+rules:
|
|
199
|
+
- DOMAIN-SUFFIX,example.internal,Tunnel-work
|
|
200
|
+
- IP-CIDR,10.0.0.0/8,Tunnel-work
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
该文件生成后**完全由你维护**,CLI 不会再改写;`tunnel rm` 也不会删它。改完执行 `mihomo start` 生效。
|
|
204
|
+
|
|
205
|
+
### 与 start / stop 的联动
|
|
206
|
+
|
|
207
|
+
隧道默认带 `auto` 标记,`mihomo start` 会顺带拉起、`mihomo stop` 会连带停止(`--no-tunnel` 可跳过;`add` 时加 `--no-auto` 则不参与)。
|
|
208
|
+
|
|
209
|
+
- **隧道起不来不会让 `start` 失败**——它只影响内网分流那部分规则,其余流量正常,但会打印显眼的黄色警告并附上 ssh 给出的原因
|
|
210
|
+
- **`stop` 只停自己起的**:手动 `tunnel up` 起的隧道不会被 `mihomo stop` 带走,避免下次 start 又起一个而累积僵尸进程
|
|
211
|
+
|
|
212
|
+
### 状态的三种形态
|
|
213
|
+
|
|
214
|
+
`tunnel status` 会**真实探测端口是否在监听**,而不是只看进程在不在:
|
|
215
|
+
|
|
216
|
+
| 状态 | 含义 |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| 运行中 | 进程在且端口在监听,可正常使用 |
|
|
219
|
+
| **假活** | 进程还在但端口不通——最需要警惕的形态,此时 mihomo 仍在往死端口送流量 |
|
|
220
|
+
| 未运行 | 进程不在 |
|
|
221
|
+
|
|
222
|
+
### 安全边界
|
|
223
|
+
|
|
224
|
+
- `-D` **恒绑 `127.0.0.1`**,不提供绑定地址开关:绑 `0.0.0.0` 会让同一 WiFi 下任何设备都能经本机进入内网
|
|
225
|
+
- ssh 参数固定带 `ExitOnForwardFailure`/`BatchMode`/`ConnectTimeout`/`ServerAlive*`,分别防「假活」、无 TTY 挂死、久等、断线后端口成僵尸
|
|
226
|
+
- `--host` 拒绝以 `-` 开头的值(`-oProxyCommand=...` 会被 ssh 当选项解析,等同任意命令执行)
|
|
227
|
+
|
|
228
|
+
> 暂不做自动重连保活。断线依靠 `ServerAliveInterval` 让 ssh 进程自行退出,再用 `tunnel status` 查出来。
|
|
229
|
+
|
|
176
230
|
## 进程保活
|
|
177
231
|
|
|
178
232
|
默认情况下,mihomo 内核在后台独立运行,但如果内核崩溃、被系统 kill(如内存不足)、或重启/重新登录后,代理就会失效且不会自动恢复。进程保活用 macOS 原生的 **launchd** 解决这个问题。
|
|
@@ -279,6 +333,7 @@ mihomo test --timeout=3000 # 长选项 + 等号
|
|
|
279
333
|
├── settings.json # 用户设置(订阅列表等)
|
|
280
334
|
├── overwrite.yaml # 覆写配置(主文件,可选)
|
|
281
335
|
├── overwrite.*.yaml # 覆写配置(扩展文件,如 overwrite.dns.yaml)
|
|
336
|
+
├── overwrite.tunnel-*.yaml # 隧道覆写(首次由 tunnel add 生成,此后由你维护)
|
|
282
337
|
├── subscriptions/
|
|
283
338
|
│ ├── cache.json # 订阅动态缓存(更新时间、流量、到期时间等)
|
|
284
339
|
│ └── <name>.yaml # 订阅原始配置
|
|
@@ -286,8 +341,11 @@ mihomo test --timeout=3000 # 长选项 + 等号
|
|
|
286
341
|
│ └── mihomo # mihomo 内核二进制
|
|
287
342
|
├── logs/
|
|
288
343
|
│ ├── mihomo.log # 当前日志
|
|
289
|
-
│
|
|
344
|
+
│ ├── mihomo.YYYY-MM-DD_HH-MM-SS.log # 归档日志
|
|
345
|
+
│ └── tunnel-<name>.log # 隧道 ssh 输出(每次启动覆写)
|
|
290
346
|
├── data/ # mihomo 运行数据(GeoIP 等,由内核自行管理)
|
|
347
|
+
├── tunnel/ # 隧道运行态(stop 不清除,故不放在 runtime/)
|
|
348
|
+
│ └── <name>.json # PID、谁启动的、启动时间
|
|
291
349
|
└── runtime/ # 运行时临时文件(stop 自动清除)
|
|
292
350
|
├── pid # 进程 PID
|
|
293
351
|
├── config.yaml # 运行时生成的配置
|
package/dist/index.js
CHANGED
|
@@ -3182,7 +3182,11 @@ var DIRS = {
|
|
|
3182
3182
|
subscriptions: path.join(USER_DATA_DIR, "subscriptions"),
|
|
3183
3183
|
logs: path.join(USER_DATA_DIR, "logs"),
|
|
3184
3184
|
data: path.join(USER_DATA_DIR, "data"),
|
|
3185
|
-
runtime: path.join(USER_DATA_DIR, "runtime")
|
|
3185
|
+
runtime: path.join(USER_DATA_DIR, "runtime"),
|
|
3186
|
+
// 隧道运行态。刻意独立于 runtime/:clearRuntime() 会在 stop() 成功路径 rmrf 整个
|
|
3187
|
+
// runtime 目录,隧道状态放那里会被 `mihomo stop` 连同 config.yaml 一起抹掉,
|
|
3188
|
+
// 于是「谁起的」标记丢失、手动起的隧道再也无法被识别
|
|
3189
|
+
tunnel: path.join(USER_DATA_DIR, "tunnel")
|
|
3186
3190
|
};
|
|
3187
3191
|
var PATHS = {
|
|
3188
3192
|
mihomoBinary: path.join(DIRS.kernel, "mihomo"),
|
|
@@ -3764,10 +3768,40 @@ function parseIntArg(args, short, long, defaultValue) {
|
|
|
3764
3768
|
}
|
|
3765
3769
|
return defaultValue;
|
|
3766
3770
|
}
|
|
3767
|
-
|
|
3771
|
+
function parseStringArg(args, long, short) {
|
|
3772
|
+
if (!args) return null;
|
|
3773
|
+
for (let i = 0; i < args.length; i++) {
|
|
3774
|
+
if (args[i] === long || short !== void 0 && args[i] === short) {
|
|
3775
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
3776
|
+
return args[i + 1];
|
|
3777
|
+
}
|
|
3778
|
+
throw new CliError(`\u9009\u9879 ${args[i]} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${long} <\u503C>`] });
|
|
3779
|
+
}
|
|
3780
|
+
if (args[i].startsWith(`${long}=`)) {
|
|
3781
|
+
const value = args[i].slice(long.length + 1);
|
|
3782
|
+
if (!value) throw new CliError(`\u9009\u9879 ${long} \u7F3A\u5C11\u503C`, { hint: [`\u4F8B\u5982: ${long}=<\u503C>`] });
|
|
3783
|
+
return value;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
return null;
|
|
3787
|
+
}
|
|
3788
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
3789
|
+
"-t",
|
|
3790
|
+
"--timeout",
|
|
3791
|
+
"-j",
|
|
3792
|
+
"--concurrency",
|
|
3793
|
+
"-r",
|
|
3794
|
+
"--rounds",
|
|
3795
|
+
"-n",
|
|
3796
|
+
"--lines",
|
|
3797
|
+
"-u",
|
|
3798
|
+
"--update-timeout",
|
|
3799
|
+
"--host",
|
|
3800
|
+
"--port"
|
|
3801
|
+
]);
|
|
3768
3802
|
function extractStartOptions(args) {
|
|
3769
3803
|
if (!args) return [];
|
|
3770
|
-
const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean"]);
|
|
3804
|
+
const BOOL_FLAGS = /* @__PURE__ */ new Set(["-s", "--no-update", "--no-clean", "--no-tunnel"]);
|
|
3771
3805
|
const out = [];
|
|
3772
3806
|
for (let i = 0; i < args.length; i++) {
|
|
3773
3807
|
const a = args[i];
|
|
@@ -4232,12 +4266,13 @@ ${colors.cyan(colors.bold(`mihomo-cli v${VERSION}`))}`, "", "\u547D\u4EE4\u522B\
|
|
|
4232
4266
|
" mihomo start -s # \u8DF3\u8FC7\u81EA\u52A8\u66F4\u65B0\u8BA2\u9605",
|
|
4233
4267
|
" mihomo start -u 30000 # \u81EA\u52A8\u66F4\u65B0\u8D85\u65F6 30 \u79D2 (\u9ED8\u8BA4 10s)",
|
|
4234
4268
|
" mihomo daemon on # \u5F00\u542F\u4FDD\u6D3B\uFF08\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF09",
|
|
4269
|
+
" mihomo tunnel add work --host m4 --port 1080 # \u52A0 ssh \u96A7\u9053\u51FA\u53E3",
|
|
4235
4270
|
" mihomo sub add <url> # \u6DFB\u52A0\u8BA2\u9605 (sub \u662F subscription \u522B\u540D)",
|
|
4236
4271
|
" mihomo ui # \u6253\u5F00 Web UI",
|
|
4237
4272
|
"",
|
|
4238
4273
|
`${colors.cyan("\u5FEB\u6377\u547D\u4EE4:")}`,
|
|
4239
4274
|
" tun = start tun use = sub use on/off = ow on/off open = dir open",
|
|
4240
|
-
" up = start down = stop upd/upgrade = update",
|
|
4275
|
+
" up = start down = stop upd/upgrade = update ssh = tunnel",
|
|
4241
4276
|
"",
|
|
4242
4277
|
`${colors.cyan("\u6A21\u5F0F\u8BF4\u660E:")}`,
|
|
4243
4278
|
" mixed HTTP + SOCKS5 \u6DF7\u5408\u7AEF\u53E3 (\u9ED8\u8BA4)",
|
|
@@ -5734,8 +5769,373 @@ function formatTestSummary(summary) {
|
|
|
5734
5769
|
return `\u7ED3\u679C: ${colors.green(`${summary.alive} \u5B58\u6D3B`)} / ${colors.red(`${summary.dead} \u5931\u8D25`)} / ${summary.total} \u603B\u8BA1`;
|
|
5735
5770
|
}
|
|
5736
5771
|
|
|
5772
|
+
// src/tunnel.ts
|
|
5773
|
+
import { spawn as spawn2 } from "child_process";
|
|
5774
|
+
import fs7 from "fs";
|
|
5775
|
+
import net from "net";
|
|
5776
|
+
import path6 from "path";
|
|
5777
|
+
var PORT_PROBE_TIMEOUT_MS = 300;
|
|
5778
|
+
var STOP_WAIT_ATTEMPTS = 20;
|
|
5779
|
+
var STOP_WAIT_INTERVAL = 100;
|
|
5780
|
+
var START_WAIT_ATTEMPTS = 40;
|
|
5781
|
+
var START_WAIT_INTERVAL = 500;
|
|
5782
|
+
function getReservedPorts() {
|
|
5783
|
+
const reserved = /* @__PURE__ */ new Map();
|
|
5784
|
+
const mixedPort = Number(BASE_CONFIG["mixed-port"]);
|
|
5785
|
+
if (Number.isInteger(mixedPort)) reserved.set(mixedPort, "mihomo \u6DF7\u5408\u4EE3\u7406\u7AEF\u53E3");
|
|
5786
|
+
reserved.set(CONTROLLER_PORT, "mihomo \u63A7\u5236\u5668\u7AEF\u53E3");
|
|
5787
|
+
const testPort = Number(TEST_CONFIG["mixed-port"]);
|
|
5788
|
+
if (Number.isInteger(testPort)) reserved.set(testPort, "\u6D4B\u901F\u5B9E\u4F8B\u4EE3\u7406\u7AEF\u53E3");
|
|
5789
|
+
const testController = Number(TEST_CONTROLLER_ADDR.split(":")[1]);
|
|
5790
|
+
if (Number.isInteger(testController)) reserved.set(testController, "\u6D4B\u901F\u5B9E\u4F8B\u63A7\u5236\u5668\u7AEF\u53E3");
|
|
5791
|
+
return reserved;
|
|
5792
|
+
}
|
|
5793
|
+
function validateTunnelName(name) {
|
|
5794
|
+
if (!name || !SAFE_NAME_RE.test(name)) {
|
|
5795
|
+
throw new CliError(`\u96A7\u9053\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`);
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
var SAFE_HOST_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]*$/;
|
|
5799
|
+
function validateTunnelHost(host) {
|
|
5800
|
+
if (!host) {
|
|
5801
|
+
throw new CliError("\u7F3A\u5C11 --host", { hint: ["\u4F8B\u5982: mihomo tunnel add work --host m4 --port 1080"] });
|
|
5802
|
+
}
|
|
5803
|
+
if (host.startsWith("-")) {
|
|
5804
|
+
throw new CliError(`\u4E3B\u673A\u540D\u65E0\u6548: "${host}"`, {
|
|
5805
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
5806
|
+
hint: ['\u4E3B\u673A\u540D\u4E0D\u80FD\u4EE5 "-" \u5F00\u5934\u2014\u2014\u5B83\u4F1A\u88AB ssh \u5F53\u4F5C\u9009\u9879\u89E3\u6790\uFF08\u5982 -oProxyCommand=... \u53EF\u6267\u884C\u4EFB\u610F\u547D\u4EE4\uFF09\u3002']
|
|
5807
|
+
});
|
|
5808
|
+
}
|
|
5809
|
+
if (!SAFE_HOST_RE.test(host)) {
|
|
5810
|
+
throw new CliError(`\u4E3B\u673A\u540D\u65E0\u6548: "${host}"\uFF0C\u53EA\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u4E0B\u5212\u7EBF\u3001\u77ED\u6A2A\u7EBF\u548C @`, {
|
|
5811
|
+
label: "\u53C2\u6570\u9519\u8BEF",
|
|
5812
|
+
hint: ["\u53EF\u7528 ssh \u522B\u540D\uFF08~/.ssh/config \u91CC\u7684 Host\uFF09\u6216 user@hostname\u3002"]
|
|
5813
|
+
});
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
function validateTunnelPort(port, exclude) {
|
|
5817
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
5818
|
+
throw new CliError(`\u7AEF\u53E3\u65E0\u6548: ${port}\uFF0C\u9700\u4E3A 1-65535 \u7684\u6574\u6570`);
|
|
5819
|
+
}
|
|
5820
|
+
const reservedLabel = getReservedPorts().get(port);
|
|
5821
|
+
if (reservedLabel) {
|
|
5822
|
+
throw new CliError(`\u7AEF\u53E3 ${port} \u5DF2\u88AB ${reservedLabel} \u5360\u7528`, {
|
|
5823
|
+
label: "\u7AEF\u53E3\u51B2\u7A81",
|
|
5824
|
+
hint: ["\u8BF7\u6362\u4E00\u4E2A\u7AEF\u53E3\uFF0C\u4F8B\u5982 1080\u3002"]
|
|
5825
|
+
});
|
|
5826
|
+
}
|
|
5827
|
+
const conflict = getTunnels().find((t) => t.port === port && t.name !== exclude);
|
|
5828
|
+
if (conflict) {
|
|
5829
|
+
throw new CliError(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u96A7\u9053 "${conflict.name}" \u4F7F\u7528`, { label: "\u7AEF\u53E3\u51B2\u7A81" });
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
function getTunnels() {
|
|
5833
|
+
const settings = readSettings();
|
|
5834
|
+
const tunnels = settings.tunnels;
|
|
5835
|
+
if (!Array.isArray(tunnels)) {
|
|
5836
|
+
if (tunnels !== void 0) {
|
|
5837
|
+
console.warn("\u8B66\u544A: settings.json \u7684 tunnels \u4E0D\u662F\u5217\u8868\uFF0C\u5DF2\u5FFD\u7565\uFF08\u53EF\u7528 mihomo tunnel add \u91CD\u65B0\u6DFB\u52A0\uFF09");
|
|
5838
|
+
}
|
|
5839
|
+
return [];
|
|
5840
|
+
}
|
|
5841
|
+
return tunnels.filter((t) => t != null && typeof t === "object" && typeof t.name === "string" && typeof t.host === "string" && Number.isInteger(t.port));
|
|
5842
|
+
}
|
|
5843
|
+
function findTunnel(name) {
|
|
5844
|
+
return getTunnels().find((t) => t.name === name);
|
|
5845
|
+
}
|
|
5846
|
+
function addTunnel(config) {
|
|
5847
|
+
validateTunnelName(config.name);
|
|
5848
|
+
validateTunnelHost(config.host);
|
|
5849
|
+
validateTunnelPort(config.port);
|
|
5850
|
+
const tunnels = [...getTunnels()];
|
|
5851
|
+
if (tunnels.some((t) => t.name === config.name)) {
|
|
5852
|
+
throw new CliError(`\u96A7\u9053 "${config.name}" \u5DF2\u5B58\u5728\uFF0C\u8BF7\u6362\u4E2A\u540D\u79F0\uFF0C\u6216\u5148\u5220\u9664\uFF08mihomo tunnel rm ${config.name}\uFF09`);
|
|
5853
|
+
}
|
|
5854
|
+
tunnels.push(config);
|
|
5855
|
+
writeSettings({ tunnels });
|
|
5856
|
+
}
|
|
5857
|
+
function removeTunnel(name) {
|
|
5858
|
+
const tunnels = [...getTunnels()];
|
|
5859
|
+
const idx = tunnels.findIndex((t) => t.name === name);
|
|
5860
|
+
if (idx < 0) return false;
|
|
5861
|
+
tunnels.splice(idx, 1);
|
|
5862
|
+
writeSettings({ tunnels });
|
|
5863
|
+
clearTunnelRuntime(name);
|
|
5864
|
+
return true;
|
|
5865
|
+
}
|
|
5866
|
+
function getTunnelRuntimePath(name) {
|
|
5867
|
+
validateTunnelName(name);
|
|
5868
|
+
return path6.join(DIRS.tunnel, `${name}.json`);
|
|
5869
|
+
}
|
|
5870
|
+
function readTunnelRuntime(name) {
|
|
5871
|
+
try {
|
|
5872
|
+
const raw = fs7.readFileSync(getTunnelRuntimePath(name), "utf8");
|
|
5873
|
+
const parsed = JSON.parse(raw);
|
|
5874
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
5875
|
+
const r = parsed;
|
|
5876
|
+
if (!Number.isInteger(r.pid) || r.pid <= 0) return null;
|
|
5877
|
+
return {
|
|
5878
|
+
pid: r.pid,
|
|
5879
|
+
started_by: r.started_by === "manual" ? "manual" : "auto",
|
|
5880
|
+
started_at: typeof r.started_at === "string" ? r.started_at : "",
|
|
5881
|
+
port: Number.isInteger(r.port) ? r.port : 0
|
|
5882
|
+
};
|
|
5883
|
+
} catch {
|
|
5884
|
+
return null;
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
function writeTunnelRuntime(name, runtime) {
|
|
5888
|
+
ensureDirs();
|
|
5889
|
+
atomicWriteFileSync(getTunnelRuntimePath(name), JSON.stringify(runtime, null, 2), { mode: 384 });
|
|
5890
|
+
}
|
|
5891
|
+
function clearTunnelRuntime(name) {
|
|
5892
|
+
try {
|
|
5893
|
+
fs7.rmSync(getTunnelRuntimePath(name), { force: true });
|
|
5894
|
+
} catch {
|
|
5895
|
+
}
|
|
5896
|
+
}
|
|
5897
|
+
function buildSshArgs(tunnel) {
|
|
5898
|
+
return [
|
|
5899
|
+
"-D",
|
|
5900
|
+
`127.0.0.1:${tunnel.port}`,
|
|
5901
|
+
"-N",
|
|
5902
|
+
"-o",
|
|
5903
|
+
"ExitOnForwardFailure=yes",
|
|
5904
|
+
"-o",
|
|
5905
|
+
"BatchMode=yes",
|
|
5906
|
+
"-o",
|
|
5907
|
+
"ServerAliveInterval=30",
|
|
5908
|
+
"-o",
|
|
5909
|
+
"ServerAliveCountMax=3",
|
|
5910
|
+
"-o",
|
|
5911
|
+
"ConnectTimeout=15",
|
|
5912
|
+
tunnel.host
|
|
5913
|
+
];
|
|
5914
|
+
}
|
|
5915
|
+
function commandNeedle(port) {
|
|
5916
|
+
return `-D 127.0.0.1:${port}`;
|
|
5917
|
+
}
|
|
5918
|
+
function getTunnelLogPath(name) {
|
|
5919
|
+
validateTunnelName(name);
|
|
5920
|
+
return path6.join(DIRS.logs, `tunnel-${name}.log`);
|
|
5921
|
+
}
|
|
5922
|
+
function isTunnelProcessAlive(pid, port) {
|
|
5923
|
+
return isProcessRunning(pid) && isProcessCommandMatching(pid, commandNeedle(port));
|
|
5924
|
+
}
|
|
5925
|
+
function isPortListening(port, timeoutMs = PORT_PROBE_TIMEOUT_MS) {
|
|
5926
|
+
return new Promise((resolve) => {
|
|
5927
|
+
const socket = new net.Socket();
|
|
5928
|
+
let settled = false;
|
|
5929
|
+
const finish = (result) => {
|
|
5930
|
+
if (settled) return;
|
|
5931
|
+
settled = true;
|
|
5932
|
+
socket.destroy();
|
|
5933
|
+
resolve(result);
|
|
5934
|
+
};
|
|
5935
|
+
socket.setTimeout(timeoutMs);
|
|
5936
|
+
socket.once("connect", () => finish(true));
|
|
5937
|
+
socket.once("timeout", () => finish(false));
|
|
5938
|
+
socket.once("error", () => finish(false));
|
|
5939
|
+
socket.connect(port, "127.0.0.1");
|
|
5940
|
+
});
|
|
5941
|
+
}
|
|
5942
|
+
async function getTunnelStatus(config) {
|
|
5943
|
+
const runtime = readTunnelRuntime(config.name);
|
|
5944
|
+
const alive = runtime !== null && isTunnelProcessAlive(runtime.pid, runtime.port || config.port);
|
|
5945
|
+
if (!alive) {
|
|
5946
|
+
return { config, state: "stopped", pid: null, started_by: null, started_at: null };
|
|
5947
|
+
}
|
|
5948
|
+
const listening = await isPortListening(config.port);
|
|
5949
|
+
return {
|
|
5950
|
+
config,
|
|
5951
|
+
state: listening ? "running" : "dead-port",
|
|
5952
|
+
pid: runtime.pid,
|
|
5953
|
+
started_by: runtime.started_by,
|
|
5954
|
+
started_at: runtime.started_at || null
|
|
5955
|
+
};
|
|
5956
|
+
}
|
|
5957
|
+
async function getAllTunnelStatus() {
|
|
5958
|
+
return Promise.all(getTunnels().map(getTunnelStatus));
|
|
5959
|
+
}
|
|
5960
|
+
async function startTunnel(name, options) {
|
|
5961
|
+
const config = findTunnel(name);
|
|
5962
|
+
if (!config) {
|
|
5963
|
+
throw new CliError(`\u672A\u627E\u5230\u96A7\u9053 "${name}"`, { hint: ["\u67E5\u770B\u5168\u90E8\u96A7\u9053: mihomo tunnel"] });
|
|
5964
|
+
}
|
|
5965
|
+
const existing = readTunnelRuntime(name);
|
|
5966
|
+
if (existing && isTunnelProcessAlive(existing.pid, existing.port || config.port)) {
|
|
5967
|
+
if (options.startedBy === "manual" && existing.started_by === "auto") {
|
|
5968
|
+
writeTunnelRuntime(name, { ...existing, started_by: "manual" });
|
|
5969
|
+
}
|
|
5970
|
+
return { alreadyRunning: true, pid: existing.pid };
|
|
5971
|
+
}
|
|
5972
|
+
if (existing) clearTunnelRuntime(name);
|
|
5973
|
+
if (await isPortListening(config.port)) {
|
|
5974
|
+
throw new CliError(`\u7AEF\u53E3 ${config.port} \u5DF2\u88AB\u5360\u7528`, {
|
|
5975
|
+
label: "\u65E0\u6CD5\u542F\u52A8\u96A7\u9053",
|
|
5976
|
+
hint: [`\u53EF\u80FD\u5DF2\u6709\u96A7\u9053\u6216\u5176\u4ED6\u7A0B\u5E8F\u5728\u76D1\u542C 127.0.0.1:${config.port}\u3002`, `\u67E5\u770B\u5360\u7528: lsof -nP -iTCP:${config.port} -sTCP:LISTEN`]
|
|
5977
|
+
});
|
|
5978
|
+
}
|
|
5979
|
+
ensureDirs();
|
|
5980
|
+
const logPath = getTunnelLogPath(name);
|
|
5981
|
+
const logFd = fs7.openSync(logPath, "w");
|
|
5982
|
+
const child = spawn2("ssh", buildSshArgs(config), {
|
|
5983
|
+
detached: true,
|
|
5984
|
+
stdio: ["ignore", logFd, logFd]
|
|
5985
|
+
});
|
|
5986
|
+
child.on("error", () => {
|
|
5987
|
+
});
|
|
5988
|
+
fs7.closeSync(logFd);
|
|
5989
|
+
child.unref();
|
|
5990
|
+
const pid = child.pid;
|
|
5991
|
+
if (!pid) {
|
|
5992
|
+
throw new CliError("\u65E0\u6CD5\u521B\u5EFA ssh \u8FDB\u7A0B", { label: "\u542F\u52A8\u96A7\u9053\u5931\u8D25", hint: ["\u8BF7\u786E\u8BA4\u5DF2\u5B89\u88C5 ssh \u5BA2\u6237\u7AEF\u3002"] });
|
|
5993
|
+
}
|
|
5994
|
+
writeTunnelRuntime(name, {
|
|
5995
|
+
pid,
|
|
5996
|
+
started_by: options.startedBy,
|
|
5997
|
+
started_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5998
|
+
port: config.port
|
|
5999
|
+
});
|
|
6000
|
+
for (let i = 0; i < START_WAIT_ATTEMPTS; i++) {
|
|
6001
|
+
if (!isProcessRunning(pid)) break;
|
|
6002
|
+
if (await isPortListening(config.port)) {
|
|
6003
|
+
return { alreadyRunning: false, pid };
|
|
6004
|
+
}
|
|
6005
|
+
await new Promise((resolve) => setTimeout(resolve, START_WAIT_INTERVAL));
|
|
6006
|
+
}
|
|
6007
|
+
clearTunnelRuntime(name);
|
|
6008
|
+
if (isProcessRunning(pid)) {
|
|
6009
|
+
try {
|
|
6010
|
+
process.kill(pid, "SIGKILL");
|
|
6011
|
+
} catch {
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
throw new CliError("ssh \u96A7\u9053\u672A\u80FD\u5EFA\u7ACB", {
|
|
6015
|
+
label: "\u542F\u52A8\u96A7\u9053\u5931\u8D25",
|
|
6016
|
+
hint: [...readLogTail(logPath), `\u5B8C\u6574\u65E5\u5FD7: ${logPath}`]
|
|
6017
|
+
});
|
|
6018
|
+
}
|
|
6019
|
+
function readLogTail(logPath, maxLines = 5) {
|
|
6020
|
+
try {
|
|
6021
|
+
const content = fs7.readFileSync(logPath, "utf8").trim();
|
|
6022
|
+
if (!content) return [];
|
|
6023
|
+
return content.split("\n").slice(-maxLines).map((line) => ` ${line.trim()}`);
|
|
6024
|
+
} catch {
|
|
6025
|
+
return [];
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
function stopTunnel(name) {
|
|
6029
|
+
const config = findTunnel(name);
|
|
6030
|
+
const runtime = readTunnelRuntime(name);
|
|
6031
|
+
if (!runtime) {
|
|
6032
|
+
clearTunnelRuntime(name);
|
|
6033
|
+
return { notRunning: true, pid: null };
|
|
6034
|
+
}
|
|
6035
|
+
const port = runtime.port || config?.port || 0;
|
|
6036
|
+
const { pid } = runtime;
|
|
6037
|
+
if (!isTunnelProcessAlive(pid, port)) {
|
|
6038
|
+
clearTunnelRuntime(name);
|
|
6039
|
+
return { notRunning: true, pid: null };
|
|
6040
|
+
}
|
|
6041
|
+
try {
|
|
6042
|
+
process.kill(pid, "SIGTERM");
|
|
6043
|
+
} catch {
|
|
6044
|
+
}
|
|
6045
|
+
for (let i = 0; i < STOP_WAIT_ATTEMPTS; i++) {
|
|
6046
|
+
if (!isProcessRunning(pid)) break;
|
|
6047
|
+
sleepSync(STOP_WAIT_INTERVAL);
|
|
6048
|
+
}
|
|
6049
|
+
if (isProcessRunning(pid) && isProcessCommandMatching(pid, commandNeedle(port))) {
|
|
6050
|
+
try {
|
|
6051
|
+
process.kill(pid, "SIGKILL");
|
|
6052
|
+
} catch {
|
|
6053
|
+
}
|
|
6054
|
+
for (let i = 0; i < STOP_WAIT_ATTEMPTS; i++) {
|
|
6055
|
+
if (!isProcessRunning(pid)) break;
|
|
6056
|
+
sleepSync(STOP_WAIT_INTERVAL);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
clearTunnelRuntime(name);
|
|
6060
|
+
return { notRunning: false, pid };
|
|
6061
|
+
}
|
|
6062
|
+
async function startAutoTunnels() {
|
|
6063
|
+
const outcomes = [];
|
|
6064
|
+
for (const tunnel of getTunnels()) {
|
|
6065
|
+
if (!tunnel.auto) continue;
|
|
6066
|
+
try {
|
|
6067
|
+
const result = await startTunnel(tunnel.name, { startedBy: "auto" });
|
|
6068
|
+
outcomes.push({ name: tunnel.name, ok: true, alreadyRunning: result.alreadyRunning });
|
|
6069
|
+
} catch (e) {
|
|
6070
|
+
outcomes.push({
|
|
6071
|
+
name: tunnel.name,
|
|
6072
|
+
ok: false,
|
|
6073
|
+
error: e instanceof CliError ? e : new CliError(e.message)
|
|
6074
|
+
});
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
return outcomes;
|
|
6078
|
+
}
|
|
6079
|
+
function stopAutoTunnels() {
|
|
6080
|
+
const stopped = [];
|
|
6081
|
+
for (const tunnel of getTunnels()) {
|
|
6082
|
+
const runtime = readTunnelRuntime(tunnel.name);
|
|
6083
|
+
if (runtime?.started_by !== "auto") continue;
|
|
6084
|
+
const result = stopTunnel(tunnel.name);
|
|
6085
|
+
if (!result.notRunning) stopped.push(tunnel.name);
|
|
6086
|
+
}
|
|
6087
|
+
return stopped;
|
|
6088
|
+
}
|
|
6089
|
+
function stopAllTunnels() {
|
|
6090
|
+
const stopped = [];
|
|
6091
|
+
for (const tunnel of getTunnels()) {
|
|
6092
|
+
const result = stopTunnel(tunnel.name);
|
|
6093
|
+
if (!result.notRunning) stopped.push(tunnel.name);
|
|
6094
|
+
}
|
|
6095
|
+
return stopped;
|
|
6096
|
+
}
|
|
6097
|
+
function getTunnelOverwritePath(name) {
|
|
6098
|
+
validateTunnelName(name);
|
|
6099
|
+
return path6.join(USER_DATA_DIR, `overwrite.tunnel-${name}.yaml`);
|
|
6100
|
+
}
|
|
6101
|
+
function renderTunnelOverwrite(tunnel) {
|
|
6102
|
+
const proxyName = `Tunnel-${tunnel.name}-Host`;
|
|
6103
|
+
const groupName = `Tunnel-${tunnel.name}`;
|
|
6104
|
+
return `# mihomo-cli \u96A7\u9053\u8986\u5199\uFF08tunnel: ${tunnel.name}\uFF09
|
|
6105
|
+
# \u672C\u6587\u4EF6\u7531 mihomo-cli \u9996\u6B21\u521B\u5EFA\uFF0C\u4E4B\u540E\u5B8C\u5168\u7531\u4F60\u7EF4\u62A4\u2014\u2014CLI \u4E0D\u4F1A\u518D\u6539\u5199\u6216\u5220\u9664\u5B83\u3002
|
|
6106
|
+
#
|
|
6107
|
+
# ~ \u662F\u300C\u6309 name \u5C31\u5730\u5408\u5E76\u300D\u8BED\u4E49\uFF1A\u4E0E\u5176\u4ED6\u8986\u5199\u6587\u4EF6\u4E2D\u7684\u540C\u540D\u8282\u70B9/\u5206\u7EC4\u5408\u5E76\uFF0C
|
|
6108
|
+
# \u4E0D\u4F9D\u8D56\u6587\u4EF6\u52A0\u8F7D\u987A\u5E8F\uFF08+proxies \u5219\u4F9D\u8D56\u5B57\u6BCD\u5E8F\uFF0C\u4F1A\u88AB\u540E\u6765\u7684\u6587\u4EF6\u538B\u8FC7\u53BB\uFF09\u3002
|
|
6109
|
+
|
|
6110
|
+
~proxies:
|
|
6111
|
+
- name: ${proxyName}
|
|
6112
|
+
type: socks5
|
|
6113
|
+
server: 127.0.0.1
|
|
6114
|
+
port: ${tunnel.port}
|
|
6115
|
+
|
|
6116
|
+
~proxy-groups:
|
|
6117
|
+
- name: ${groupName}
|
|
6118
|
+
type: select
|
|
6119
|
+
proxies:
|
|
6120
|
+
- ${proxyName}
|
|
6121
|
+
- DIRECT
|
|
6122
|
+
|
|
6123
|
+
# \u53D6\u6D88\u6CE8\u91CA\u5E76\u586B\u5165\u9700\u8981\u8D70\u96A7\u9053\u7684\u5185\u7F51\u57DF\u540D/\u7F51\u6BB5\uFF08CLI \u65E0\u4ECE\u77E5\u9053\u4F60\u7684\u5185\u7F51\u5730\u5740\uFF09\uFF1A
|
|
6124
|
+
# +rules:
|
|
6125
|
+
# - DOMAIN-SUFFIX,example.internal,${groupName}
|
|
6126
|
+
# - IP-CIDR,10.0.0.0/8,${groupName}
|
|
6127
|
+
`;
|
|
6128
|
+
}
|
|
6129
|
+
function ensureTunnelOverwriteFile(tunnel) {
|
|
6130
|
+
const filePath = getTunnelOverwritePath(tunnel.name);
|
|
6131
|
+
if (fs7.existsSync(filePath)) return false;
|
|
6132
|
+
ensureDirs();
|
|
6133
|
+
atomicWriteFileSync(filePath, renderTunnelOverwrite(tunnel), { mode: 384 });
|
|
6134
|
+
return true;
|
|
6135
|
+
}
|
|
6136
|
+
|
|
5737
6137
|
// src/commands/status.ts
|
|
5738
|
-
function printStatus() {
|
|
6138
|
+
async function printStatus() {
|
|
5739
6139
|
const status = getStatus();
|
|
5740
6140
|
const state = getRunningState();
|
|
5741
6141
|
const info = getConfigInfo();
|
|
@@ -5802,6 +6202,17 @@ function printStatus() {
|
|
|
5802
6202
|
if (isDaemonEnabled()) {
|
|
5803
6203
|
console.log(`${colors.gray("\u4FDD\u6D3B: ")}${colors.green("\u5DF2\u542F\u7528")} ${colors.gray("(\u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F)")}`);
|
|
5804
6204
|
}
|
|
6205
|
+
const tunnels = getTunnels();
|
|
6206
|
+
if (tunnels.length > 0) {
|
|
6207
|
+
const statuses = await getAllTunnelStatus();
|
|
6208
|
+
const parts = statuses.map((s) => {
|
|
6209
|
+
const label = `${s.config.name}:${s.config.port}`;
|
|
6210
|
+
if (s.state === "running") return colors.green(label);
|
|
6211
|
+
if (s.state === "dead-port") return colors.yellow(`${label} \u5047\u6D3B`);
|
|
6212
|
+
return colors.gray(label);
|
|
6213
|
+
});
|
|
6214
|
+
console.log(`${colors.gray("\u96A7\u9053: ")}${parts.join(", ")}`);
|
|
6215
|
+
}
|
|
5805
6216
|
console.log("");
|
|
5806
6217
|
}
|
|
5807
6218
|
|
|
@@ -5811,23 +6222,51 @@ function handleStopResult(result) {
|
|
|
5811
6222
|
throw new CliError(result.remaining.join(", "), { label: "\u90E8\u5206\u8FDB\u7A0B\u672A\u7EC8\u6B62", hint: "\u8BF7\u624B\u52A8\u8FD0\u884C: sudo pkill -9 mihomo" });
|
|
5812
6223
|
}
|
|
5813
6224
|
}
|
|
5814
|
-
|
|
6225
|
+
function stopAutoTunnelsWithLog() {
|
|
6226
|
+
const stopped = stopAutoTunnels();
|
|
6227
|
+
if (stopped.length > 0) {
|
|
6228
|
+
console.log(`${colors.green("\u5DF2\u505C\u6B62\u96A7\u9053")}: ${stopped.join(", ")}`);
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
async function cmdStop(args) {
|
|
5815
6232
|
if (isDaemonEnabled()) {
|
|
5816
6233
|
console.log(colors.yellow("\u4FDD\u6D3B\u5DF2\u542F\u7528\uFF0C\u4EE3\u7406\u7531 launchd \u6258\u7BA1"));
|
|
5817
6234
|
console.log("\u76F4\u63A5\u505C\u6B62\u4F1A\u88AB\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\uFF0C\u8BF7\u7528: mihomo daemon off");
|
|
5818
6235
|
return;
|
|
5819
6236
|
}
|
|
6237
|
+
const skipTunnel = hasFlag(args, "--no-tunnel");
|
|
5820
6238
|
const pids = getMihomoPids();
|
|
5821
6239
|
if (pids.length === 0) {
|
|
5822
6240
|
console.log(colors.yellow("\u4E0D\u5728\u8FD0\u884C"));
|
|
6241
|
+
if (!skipTunnel) stopAutoTunnelsWithLog();
|
|
5823
6242
|
return;
|
|
5824
6243
|
}
|
|
5825
6244
|
console.log(`\u505C\u6B62 ${pids.length} \u4E2A\u8FDB\u7A0B...`);
|
|
5826
6245
|
handleStopResult(stop());
|
|
5827
6246
|
console.log(colors.green("\u5DF2\u505C\u6B62\u8FDB\u7A0B"));
|
|
6247
|
+
if (!skipTunnel) stopAutoTunnelsWithLog();
|
|
5828
6248
|
}
|
|
5829
6249
|
|
|
5830
6250
|
// src/commands/start.ts
|
|
6251
|
+
async function startAutoTunnelsWithWarning() {
|
|
6252
|
+
const outcomes = await startAutoTunnels();
|
|
6253
|
+
if (outcomes.length === 0) return;
|
|
6254
|
+
const started = outcomes.filter((o) => o.ok && !o.alreadyRunning);
|
|
6255
|
+
if (started.length > 0) {
|
|
6256
|
+
console.log(`${colors.green("\u5DF2\u542F\u52A8\u96A7\u9053")}: ${started.map((o) => o.name).join(", ")}`);
|
|
6257
|
+
}
|
|
6258
|
+
for (const failed of outcomes.filter((o) => !o.ok)) {
|
|
6259
|
+
console.log("");
|
|
6260
|
+
console.log(colors.yellow(`\u8B66\u544A: \u96A7\u9053 "${failed.name}" \u542F\u52A8\u5931\u8D25`));
|
|
6261
|
+
console.log(colors.gray(` ${failed.error?.message ?? "\u672A\u77E5\u9519\u8BEF"}`));
|
|
6262
|
+
for (const line of failed.error?.hint ?? []) {
|
|
6263
|
+
if (line.trim()) console.log(colors.gray(line.startsWith(" ") ? line : ` ${line}`));
|
|
6264
|
+
}
|
|
6265
|
+
console.log(colors.gray(" \u5185\u7F51\u5206\u6D41\u89C4\u5219\u5C06\u4E0D\u53EF\u7528\uFF0C\u5176\u4F59\u6D41\u91CF\u6B63\u5E38"));
|
|
6266
|
+
console.log(colors.gray(` \u6392\u67E5: mihomo tunnel status ${failed.name}`));
|
|
6267
|
+
console.log("");
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
5831
6270
|
async function cmdStart(args) {
|
|
5832
6271
|
const modeToken = args[1] && !args[1].startsWith("-") ? args[1].toLowerCase() : void 0;
|
|
5833
6272
|
if (modeToken !== void 0 && modeToken !== "tun" && modeToken !== "mixed") {
|
|
@@ -5846,6 +6285,7 @@ async function cmdStart(args) {
|
|
|
5846
6285
|
const concurrency = parseIntArg(args, "-j", "--concurrency", DEFAULT_TEST_CONCURRENCY);
|
|
5847
6286
|
const skipUpdate = hasFlag(args, "-s", "--no-update");
|
|
5848
6287
|
const skipClean = hasFlag(args, "--no-clean");
|
|
6288
|
+
const skipTunnel = hasFlag(args, "--no-tunnel");
|
|
5849
6289
|
const updateTimeout = parseIntArg(args, "-u", "--update-timeout", DEFAULT_AUTO_UPDATE_TIMEOUT);
|
|
5850
6290
|
const sub = requireActiveSubscription("\u6CA1\u6709\u8BA2\u9605\uFF0C\u8BF7\u5148\u6DFB\u52A0\u8BA2\u9605");
|
|
5851
6291
|
if (!skipUpdate) {
|
|
@@ -5887,6 +6327,9 @@ async function cmdStart(args) {
|
|
|
5887
6327
|
const lines = e.message.split("\n");
|
|
5888
6328
|
throw new CliError(lines[0], { label: "\u542F\u52A8\u5931\u8D25", hint: lines.slice(1) });
|
|
5889
6329
|
}
|
|
6330
|
+
if (!skipTunnel) {
|
|
6331
|
+
await startAutoTunnelsWithWarning();
|
|
6332
|
+
}
|
|
5890
6333
|
const cleanThreshold = isGithubUrl(sub.url) ? AUTO_CLEAN_THRESHOLD_GITHUB : AUTO_CLEAN_THRESHOLD;
|
|
5891
6334
|
if (!skipClean && configInfo.proxies > cleanThreshold) {
|
|
5892
6335
|
const cache = readSubscriptionCache();
|
|
@@ -5926,7 +6369,7 @@ async function cmdStart(args) {
|
|
|
5926
6369
|
saveSubscriptionCache(sub.name, { last_auto_clean_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
5927
6370
|
}
|
|
5928
6371
|
}
|
|
5929
|
-
printStatus();
|
|
6372
|
+
await printStatus();
|
|
5930
6373
|
}
|
|
5931
6374
|
|
|
5932
6375
|
// src/commands/shared.ts
|
|
@@ -6133,8 +6576,8 @@ async function cmdDirectory(args) {
|
|
|
6133
6576
|
|
|
6134
6577
|
// src/kernel.ts
|
|
6135
6578
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
6136
|
-
import
|
|
6137
|
-
import
|
|
6579
|
+
import fs8 from "fs";
|
|
6580
|
+
import path7 from "path";
|
|
6138
6581
|
|
|
6139
6582
|
// node_modules/compare-versions/lib/esm/utils.js
|
|
6140
6583
|
var semver = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i;
|
|
@@ -6260,10 +6703,10 @@ async function checkUpdate(mirror) {
|
|
|
6260
6703
|
}
|
|
6261
6704
|
function findBinaryInDir(dir, maxDepth = 4) {
|
|
6262
6705
|
if (maxDepth <= 0) return null;
|
|
6263
|
-
const files =
|
|
6706
|
+
const files = fs8.readdirSync(dir);
|
|
6264
6707
|
for (const f of files) {
|
|
6265
|
-
const fullPath =
|
|
6266
|
-
const stat =
|
|
6708
|
+
const fullPath = path7.join(dir, f);
|
|
6709
|
+
const stat = fs8.statSync(fullPath);
|
|
6267
6710
|
if (stat.isDirectory()) {
|
|
6268
6711
|
const found = findBinaryInDir(fullPath, maxDepth - 1);
|
|
6269
6712
|
if (found) return found;
|
|
@@ -6289,7 +6732,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6289
6732
|
\u5E73\u53F0: ${platform}, \u67B6\u6784: ${arch}${hint}`);
|
|
6290
6733
|
}
|
|
6291
6734
|
const downloadUrl = withMirror(asset.browser_download_url, mirror);
|
|
6292
|
-
const tempPath =
|
|
6735
|
+
const tempPath = path7.join(DIRS.kernel, path7.basename(asset.name));
|
|
6293
6736
|
const sizeMB = (asset.size / 1024 / 1024).toFixed(2);
|
|
6294
6737
|
if (mirror && progressCallback) {
|
|
6295
6738
|
progressCallback("\u63D0\u793A: \u7ECF\u7B2C\u4E09\u65B9\u955C\u50CF\u4E2D\u8F6C\u4E0B\u8F7D\uFF0C\u65E0\u6CD5\u9A8C\u8BC1\u6765\u6E90\u5B8C\u6574\u6027\uFF0C\u5EFA\u8BAE\u76F4\u8FDE\u6216\u81EA\u884C\u6821\u9A8C\u4EA7\u7269");
|
|
@@ -6310,12 +6753,12 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6310
6753
|
}
|
|
6311
6754
|
if (curlResult.status !== 0) {
|
|
6312
6755
|
try {
|
|
6313
|
-
|
|
6756
|
+
fs8.unlinkSync(tempPath);
|
|
6314
6757
|
} catch {
|
|
6315
6758
|
}
|
|
6316
6759
|
throw new Error(`\u4E0B\u8F7D\u5931\u8D25 (curl \u9000\u51FA\u7801 ${curlResult.status})`);
|
|
6317
6760
|
}
|
|
6318
|
-
if (!
|
|
6761
|
+
if (!fs8.existsSync(tempPath)) {
|
|
6319
6762
|
throw new Error("\u4E0B\u8F7D\u5931\u8D25: \u6587\u4EF6\u672A\u751F\u6210");
|
|
6320
6763
|
}
|
|
6321
6764
|
if (progressCallback) {
|
|
@@ -6338,17 +6781,17 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6338
6781
|
if (tarResult.error) throw tarResult.error;
|
|
6339
6782
|
if (tarResult.status !== 0) throw new Error(`tar \u9000\u51FA\u7801 ${tarResult.status}`);
|
|
6340
6783
|
} else if (tempPath.endsWith(".gz")) {
|
|
6341
|
-
const baseName =
|
|
6342
|
-
const outputPath =
|
|
6784
|
+
const baseName = path7.basename(tempPath, ".gz");
|
|
6785
|
+
const outputPath = path7.join(extractPath, baseName);
|
|
6343
6786
|
const gzipResult = spawnSync4("gzip", ["-dc", tempPath], { maxBuffer: 256 * 1024 * 1024, timeout: 6e4 });
|
|
6344
6787
|
if (gzipResult.error) throw gzipResult.error;
|
|
6345
6788
|
if (gzipResult.status !== 0) throw new Error(`gzip \u9000\u51FA\u7801 ${gzipResult.status}`);
|
|
6346
|
-
|
|
6789
|
+
fs8.writeFileSync(outputPath, gzipResult.stdout, { mode: 493 });
|
|
6347
6790
|
extractedBinary = outputPath;
|
|
6348
6791
|
}
|
|
6349
6792
|
} catch (e) {
|
|
6350
6793
|
try {
|
|
6351
|
-
|
|
6794
|
+
fs8.unlinkSync(tempPath);
|
|
6352
6795
|
} catch {
|
|
6353
6796
|
}
|
|
6354
6797
|
throw new Error(`\u89E3\u538B\u5931\u8D25: ${e.message}`);
|
|
@@ -6356,23 +6799,23 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6356
6799
|
const foundBinary = extractedBinary || findBinaryInDir(extractPath);
|
|
6357
6800
|
if (!foundBinary) {
|
|
6358
6801
|
try {
|
|
6359
|
-
|
|
6802
|
+
fs8.unlinkSync(tempPath);
|
|
6360
6803
|
} catch {
|
|
6361
6804
|
}
|
|
6362
6805
|
throw new Error("\u89E3\u538B\u540E\u672A\u627E\u5230\u53EF\u6267\u884C\u6587\u4EF6");
|
|
6363
6806
|
}
|
|
6364
6807
|
const targetPath = PATHS.mihomoBinary;
|
|
6365
6808
|
if (foundBinary !== targetPath) {
|
|
6366
|
-
if (
|
|
6367
|
-
|
|
6809
|
+
if (fs8.existsSync(targetPath)) {
|
|
6810
|
+
fs8.chmodSync(targetPath, 493);
|
|
6368
6811
|
try {
|
|
6369
|
-
|
|
6812
|
+
fs8.unlinkSync(targetPath);
|
|
6370
6813
|
} catch {
|
|
6371
6814
|
}
|
|
6372
6815
|
}
|
|
6373
|
-
|
|
6816
|
+
fs8.renameSync(foundBinary, targetPath);
|
|
6374
6817
|
}
|
|
6375
|
-
|
|
6818
|
+
fs8.chmodSync(targetPath, 493);
|
|
6376
6819
|
if (progressCallback) {
|
|
6377
6820
|
progressCallback("\u6821\u9A8C\u5185\u6838...");
|
|
6378
6821
|
}
|
|
@@ -6380,11 +6823,11 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6380
6823
|
const checkOutput = `${check.stdout || ""}${check.stderr || ""}`.trim();
|
|
6381
6824
|
if (check.error || check.status !== 0 || !/v?\d+\.\d+\.\d+/.test(checkOutput)) {
|
|
6382
6825
|
try {
|
|
6383
|
-
|
|
6826
|
+
fs8.unlinkSync(targetPath);
|
|
6384
6827
|
} catch {
|
|
6385
6828
|
}
|
|
6386
6829
|
try {
|
|
6387
|
-
|
|
6830
|
+
fs8.unlinkSync(tempPath);
|
|
6388
6831
|
} catch {
|
|
6389
6832
|
}
|
|
6390
6833
|
throw new Error(`\u5185\u6838\u81EA\u68C0\u5931\u8D25\uFF08\u53EF\u80FD\u4E0B\u8F7D\u635F\u574F\u6216\u67B6\u6784\u4E0D\u5339\u914D\uFF09\uFF0C\u5DF2\u5220\u9664
|
|
@@ -6392,7 +6835,7 @@ async function downloadKernel(progressCallback, mirror, releaseInfo) {
|
|
|
6392
6835
|
\u8F93\u51FA: ${checkOutput || "(\u7A7A)"}`);
|
|
6393
6836
|
}
|
|
6394
6837
|
try {
|
|
6395
|
-
|
|
6838
|
+
fs8.unlinkSync(tempPath);
|
|
6396
6839
|
} catch {
|
|
6397
6840
|
}
|
|
6398
6841
|
clearKernelVersionCache();
|
|
@@ -6527,7 +6970,7 @@ function cmdLogs(args) {
|
|
|
6527
6970
|
}
|
|
6528
6971
|
|
|
6529
6972
|
// src/commands/overwrite.ts
|
|
6530
|
-
import
|
|
6973
|
+
import path8 from "path";
|
|
6531
6974
|
function printOverwriteList() {
|
|
6532
6975
|
const info = listOverwriteFile();
|
|
6533
6976
|
const statusText = info.enabled ? colors.green("\u5DF2\u542F\u7528") : colors.yellow("\u5DF2\u7981\u7528");
|
|
@@ -6537,8 +6980,8 @@ function printOverwriteList() {
|
|
|
6537
6980
|
if (info.files.length === 0) {
|
|
6538
6981
|
console.log("\u6682\u65E0\u8986\u5199\u6587\u4EF6");
|
|
6539
6982
|
console.log("");
|
|
6540
|
-
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${
|
|
6541
|
-
console.log(` \u6216 ${
|
|
6983
|
+
console.log(`\u7528\u6CD5\u793A\u4F8B: \u521B\u5EFA\u6587\u4EF6 ${path8.join(info.dir, "overwrite.yaml")}`);
|
|
6984
|
+
console.log(` \u6216 ${path8.join(info.dir, "overwrite.dns.yaml")}`);
|
|
6542
6985
|
console.log("");
|
|
6543
6986
|
} else {
|
|
6544
6987
|
console.log(`${colors.cyan("\u8986\u5199\u6587\u4EF6")} (${info.files.length} \u4E2A\uFF0C\u6309\u987A\u5E8F\u52A0\u8F7D):`);
|
|
@@ -6602,7 +7045,7 @@ async function cmdOverwrite(args) {
|
|
|
6602
7045
|
}
|
|
6603
7046
|
|
|
6604
7047
|
// src/commands/reset.ts
|
|
6605
|
-
import
|
|
7048
|
+
import fs9 from "fs";
|
|
6606
7049
|
var RESET_TARGETS = [
|
|
6607
7050
|
{
|
|
6608
7051
|
id: "subs",
|
|
@@ -6635,6 +7078,21 @@ var RESET_TARGETS = [
|
|
|
6635
7078
|
paths: () => [DIRS.runtime],
|
|
6636
7079
|
needsStop: true
|
|
6637
7080
|
},
|
|
7081
|
+
{
|
|
7082
|
+
// 隧道要在删 pid 文件之前先停进程(onBefore):文件一删就再也找不到那些 ssh 进程,
|
|
7083
|
+
// 它们会继续占着端口跑下去,且 CLI 无任何路径能停掉。
|
|
7084
|
+
// **必须排在 settings 之前**:onAfter 会 writeSettings 重建 settings.json,
|
|
7085
|
+
// 排在 settings 之后会让 `reset --full` 留下一个 {}(同 subs 的处理,见执行顺序注释)
|
|
7086
|
+
id: "tunnel",
|
|
7087
|
+
aliases: ["tunnel", "tunnels", "ssh"],
|
|
7088
|
+
label: "\u96A7\u9053",
|
|
7089
|
+
paths: () => [DIRS.tunnel],
|
|
7090
|
+
needsStop: false,
|
|
7091
|
+
onBefore: () => stopAllTunnels(),
|
|
7092
|
+
// 同步清空 settings 里的隧道列表:只删运行态会留下「列表在但状态没了」的半重置状态。
|
|
7093
|
+
// 覆写文件不动——那是用户维护的资产,由 overwrites target 负责
|
|
7094
|
+
onAfter: () => writeSettings({ tunnels: void 0 })
|
|
7095
|
+
},
|
|
6638
7096
|
{
|
|
6639
7097
|
id: "settings",
|
|
6640
7098
|
aliases: ["setting", "settings", "config"],
|
|
@@ -6662,8 +7120,8 @@ var RESET_TARGETS = [
|
|
|
6662
7120
|
label: "\u8986\u5199",
|
|
6663
7121
|
paths: () => {
|
|
6664
7122
|
const dir = USER_DATA_DIR;
|
|
6665
|
-
if (!
|
|
6666
|
-
return
|
|
7123
|
+
if (!fs9.existsSync(dir)) return [];
|
|
7124
|
+
return fs9.readdirSync(dir).filter(isOverwriteFilename).map((f) => `${dir}/${f}`);
|
|
6667
7125
|
},
|
|
6668
7126
|
needsStop: false
|
|
6669
7127
|
},
|
|
@@ -6726,7 +7184,7 @@ async function cmdReset(args) {
|
|
|
6726
7184
|
}
|
|
6727
7185
|
targets = matched;
|
|
6728
7186
|
} else {
|
|
6729
|
-
targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon"].includes(t.id));
|
|
7187
|
+
targets = RESET_TARGETS.filter((t) => !["settings", "kernel", "overwrites", "daemon", "tunnel"].includes(t.id));
|
|
6730
7188
|
}
|
|
6731
7189
|
for (const t of targets) {
|
|
6732
7190
|
if (t.checkEmpty?.()) {
|
|
@@ -6786,8 +7244,9 @@ async function cmdReset(args) {
|
|
|
6786
7244
|
}
|
|
6787
7245
|
}
|
|
6788
7246
|
for (const t of targets) {
|
|
7247
|
+
t.onBefore?.();
|
|
6789
7248
|
for (const p of t.paths()) {
|
|
6790
|
-
if (
|
|
7249
|
+
if (fs9.existsSync(p)) {
|
|
6791
7250
|
try {
|
|
6792
7251
|
rmrf(p);
|
|
6793
7252
|
} catch (e) {
|
|
@@ -6805,9 +7264,9 @@ async function cmdReset(args) {
|
|
|
6805
7264
|
}
|
|
6806
7265
|
|
|
6807
7266
|
// src/test-instance.ts
|
|
6808
|
-
import { spawn as
|
|
6809
|
-
import
|
|
6810
|
-
import
|
|
7267
|
+
import { spawn as spawn3 } from "child_process";
|
|
7268
|
+
import fs10 from "fs";
|
|
7269
|
+
import path9 from "path";
|
|
6811
7270
|
function isProxyValid(proxy) {
|
|
6812
7271
|
if (proxy === null || typeof proxy !== "object") return false;
|
|
6813
7272
|
if (!proxy.name || !proxy.server || !proxy.port) return false;
|
|
@@ -6818,20 +7277,20 @@ function isProxyValid(proxy) {
|
|
|
6818
7277
|
}
|
|
6819
7278
|
return true;
|
|
6820
7279
|
}
|
|
6821
|
-
var TEST_DIR =
|
|
7280
|
+
var TEST_DIR = path9.join(USER_DATA_DIR, "test");
|
|
6822
7281
|
var TEST_DIRS = {
|
|
6823
|
-
data:
|
|
6824
|
-
runtime:
|
|
7282
|
+
data: path9.join(TEST_DIR, "data"),
|
|
7283
|
+
runtime: path9.join(TEST_DIR, "runtime")
|
|
6825
7284
|
};
|
|
6826
7285
|
var TEST_PATHS = {
|
|
6827
|
-
configFile:
|
|
6828
|
-
pidFile:
|
|
6829
|
-
logFile:
|
|
7286
|
+
configFile: path9.join(TEST_DIRS.runtime, "config.yaml"),
|
|
7287
|
+
pidFile: path9.join(TEST_DIRS.runtime, "pid"),
|
|
7288
|
+
logFile: path9.join(TEST_DIR, "test.log")
|
|
6830
7289
|
};
|
|
6831
7290
|
var TEST_API = `http://${TEST_CONFIG["external-controller"]}`;
|
|
6832
7291
|
function ensureTestDirs() {
|
|
6833
7292
|
for (const dir of Object.values(TEST_DIRS)) {
|
|
6834
|
-
|
|
7293
|
+
fs10.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
6835
7294
|
}
|
|
6836
7295
|
}
|
|
6837
7296
|
function cleanupTestDir() {
|
|
@@ -6869,25 +7328,25 @@ function buildTestConfig(subName) {
|
|
|
6869
7328
|
rules: ["MATCH,PROXY"]
|
|
6870
7329
|
};
|
|
6871
7330
|
const content = dumpYaml(config);
|
|
6872
|
-
|
|
7331
|
+
fs10.writeFileSync(TEST_PATHS.configFile, content, { mode: 384 });
|
|
6873
7332
|
}
|
|
6874
7333
|
async function startTestInstance() {
|
|
6875
7334
|
const binary = PATHS.mihomoBinary;
|
|
6876
|
-
if (!
|
|
7335
|
+
if (!fs10.existsSync(binary)) throw new CliError('\u672A\u627E\u5230 mihomo \u5185\u6838\uFF0C\u8BF7\u5148\u8FD0\u884C "mihomo kernel" \u4E0B\u8F7D');
|
|
6877
7336
|
stopTestInstance();
|
|
6878
|
-
const logFd =
|
|
6879
|
-
const child =
|
|
7337
|
+
const logFd = fs10.openSync(TEST_PATHS.logFile, "a");
|
|
7338
|
+
const child = spawn3(binary, ["-d", TEST_DIRS.data, "-f", TEST_PATHS.configFile], {
|
|
6880
7339
|
detached: true,
|
|
6881
7340
|
stdio: ["ignore", logFd, logFd]
|
|
6882
7341
|
});
|
|
6883
7342
|
child.on("error", () => {
|
|
6884
7343
|
});
|
|
6885
|
-
|
|
7344
|
+
fs10.closeSync(logFd);
|
|
6886
7345
|
child.unref();
|
|
6887
7346
|
const pid = child.pid;
|
|
6888
7347
|
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");
|
|
6889
7348
|
spawnedTestPid = pid;
|
|
6890
|
-
|
|
7349
|
+
fs10.writeFileSync(TEST_PATHS.pidFile, pid.toString(), { mode: 384 });
|
|
6891
7350
|
const client = createHttpClient({ timeout: 2e3 });
|
|
6892
7351
|
let ready = false;
|
|
6893
7352
|
for (let i = 0; i < 60; i++) {
|
|
@@ -6903,7 +7362,7 @@ async function startTestInstance() {
|
|
|
6903
7362
|
if (!isProcessRunning(pid)) {
|
|
6904
7363
|
let errorDetail = "";
|
|
6905
7364
|
try {
|
|
6906
|
-
errorDetail =
|
|
7365
|
+
errorDetail = fs10.readFileSync(TEST_PATHS.logFile, "utf8").slice(-1e3);
|
|
6907
7366
|
} catch {
|
|
6908
7367
|
}
|
|
6909
7368
|
throw new CliError(`\u6D4B\u8BD5\u5B9E\u4F8B\u542F\u52A8\u5931\u8D25${errorDetail ? `
|
|
@@ -6917,7 +7376,7 @@ var spawnedTestPid = null;
|
|
|
6917
7376
|
function stopTestInstance() {
|
|
6918
7377
|
let pid = null;
|
|
6919
7378
|
try {
|
|
6920
|
-
const fromFile = parseInt(
|
|
7379
|
+
const fromFile = parseInt(fs10.readFileSync(TEST_PATHS.pidFile, "utf8").trim(), 10);
|
|
6921
7380
|
if (fromFile > 0) pid = fromFile;
|
|
6922
7381
|
} catch {
|
|
6923
7382
|
}
|
|
@@ -6932,7 +7391,7 @@ function stopTestInstance() {
|
|
|
6932
7391
|
}
|
|
6933
7392
|
spawnedTestPid = null;
|
|
6934
7393
|
try {
|
|
6935
|
-
|
|
7394
|
+
fs10.unlinkSync(TEST_PATHS.pidFile);
|
|
6936
7395
|
} catch {
|
|
6937
7396
|
}
|
|
6938
7397
|
}
|
|
@@ -7342,6 +7801,224 @@ async function cmdClean(args) {
|
|
|
7342
7801
|
}
|
|
7343
7802
|
}
|
|
7344
7803
|
|
|
7804
|
+
// src/commands/tunnel.ts
|
|
7805
|
+
function formatState(status) {
|
|
7806
|
+
switch (status.state) {
|
|
7807
|
+
case "running":
|
|
7808
|
+
return colors.green(`\u8FD0\u884C\u4E2D (PID ${status.pid})`);
|
|
7809
|
+
case "dead-port":
|
|
7810
|
+
return colors.yellow(`\u5047\u6D3B (PID ${status.pid}\uFF0C\u7AEF\u53E3 ${status.config.port} \u4E0D\u901A)`);
|
|
7811
|
+
default:
|
|
7812
|
+
return colors.yellow("\u672A\u8FD0\u884C");
|
|
7813
|
+
}
|
|
7814
|
+
}
|
|
7815
|
+
function getTunnelNameArg(args) {
|
|
7816
|
+
return getNonFlagArg(args, 2);
|
|
7817
|
+
}
|
|
7818
|
+
function requireTunnelNameArg(args, usage) {
|
|
7819
|
+
const name = getTunnelNameArg(args);
|
|
7820
|
+
if (!name) {
|
|
7821
|
+
const tunnels = getTunnels();
|
|
7822
|
+
throw new CliError("\u8BF7\u6307\u5B9A\u96A7\u9053\u540D\u79F0", {
|
|
7823
|
+
hint: [usage, ...tunnels.length > 0 ? ["", "\u53EF\u7528\u96A7\u9053:", ...tunnels.map((t) => ` ${t.name}`)] : []]
|
|
7824
|
+
});
|
|
7825
|
+
}
|
|
7826
|
+
return name;
|
|
7827
|
+
}
|
|
7828
|
+
async function printTunnelList() {
|
|
7829
|
+
const tunnels = getTunnels();
|
|
7830
|
+
console.log("");
|
|
7831
|
+
if (tunnels.length === 0) {
|
|
7832
|
+
console.log("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053");
|
|
7833
|
+
console.log("");
|
|
7834
|
+
console.log("\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>");
|
|
7835
|
+
console.log(colors.gray(" \u4F8B\u5982: mihomo tunnel add work --host m4 --port 1080"));
|
|
7836
|
+
console.log(colors.gray(" \u96A7\u9053\u628A\u5185\u7F51\u51FA\u53E3\u66B4\u9732\u4E3A\u672C\u5730 SOCKS5\uFF0C\u914D\u5408\u8986\u5199\u6587\u4EF6\u5206\u6D41\u5185\u7F51\u57DF\u540D"));
|
|
7837
|
+
console.log("");
|
|
7838
|
+
return;
|
|
7839
|
+
}
|
|
7840
|
+
const statuses = await getAllTunnelStatus();
|
|
7841
|
+
console.log(colors.cyan("\u96A7\u9053\u5217\u8868:"));
|
|
7842
|
+
console.log("");
|
|
7843
|
+
for (const status of statuses) {
|
|
7844
|
+
const { config } = status;
|
|
7845
|
+
const autoLabel = config.auto ? colors.gray(" [auto]") : "";
|
|
7846
|
+
console.log(` ${colors.bold(config.name)}${autoLabel}`);
|
|
7847
|
+
console.log(` ${colors.gray("\u51FA\u53E3: ")}${config.host} \u2192 127.0.0.1:${config.port}`);
|
|
7848
|
+
console.log(` ${colors.gray("\u72B6\u6001: ")}${formatState(status)}`);
|
|
7849
|
+
if (status.started_by) {
|
|
7850
|
+
console.log(` ${colors.gray("\u6765\u6E90: ")}${status.started_by === "auto" ? "\u968F start \u62C9\u8D77" : "\u624B\u52A8\u542F\u52A8"}`);
|
|
7851
|
+
}
|
|
7852
|
+
}
|
|
7853
|
+
console.log("");
|
|
7854
|
+
console.log("\u542F\u52A8: mihomo tunnel up [\u540D\u5B57] \u505C\u6B62: mihomo tunnel down [\u540D\u5B57]");
|
|
7855
|
+
console.log("\u72B6\u6001: mihomo tunnel status \u5220\u9664: mihomo tunnel rm <\u540D\u5B57>");
|
|
7856
|
+
console.log("");
|
|
7857
|
+
}
|
|
7858
|
+
async function tunnelAdd(args) {
|
|
7859
|
+
const name = requireTunnelNameArg(args, "\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3> [--no-auto]");
|
|
7860
|
+
const host = parseStringArg(args, "--host");
|
|
7861
|
+
if (!host) {
|
|
7862
|
+
throw new CliError("\u7F3A\u5C11 --host", {
|
|
7863
|
+
hint: ["\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>", "", "\u4E3B\u673A\u53EF\u7528 ~/.ssh/config \u91CC\u7684\u522B\u540D\uFF0C\u4F8B\u5982 --host m4"]
|
|
7864
|
+
});
|
|
7865
|
+
}
|
|
7866
|
+
const portRaw = parseStringArg(args, "--port");
|
|
7867
|
+
if (!portRaw) {
|
|
7868
|
+
throw new CliError("\u7F3A\u5C11 --port", { hint: ["\u7528\u6CD5: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"] });
|
|
7869
|
+
}
|
|
7870
|
+
if (!/^\d+$/.test(portRaw.trim())) {
|
|
7871
|
+
throw new CliError(`\u7AEF\u53E3\u65E0\u6548: "${portRaw}"\uFF0C\u9700\u4E3A 1-65535 \u7684\u6574\u6570`);
|
|
7872
|
+
}
|
|
7873
|
+
const port = Number(portRaw.trim());
|
|
7874
|
+
const auto = !hasFlag(args, "--no-auto");
|
|
7875
|
+
const config = { name, host, port, auto };
|
|
7876
|
+
addTunnel(config);
|
|
7877
|
+
const created = ensureTunnelOverwriteFile(config);
|
|
7878
|
+
const overwritePath = getTunnelOverwritePath(name);
|
|
7879
|
+
console.log(`${colors.green("\u5DF2\u6DFB\u52A0\u96A7\u9053")} ${name} \xB7 ${host} \u2192 127.0.0.1:${port}${auto ? " \xB7 auto" : ""}`);
|
|
7880
|
+
console.log("");
|
|
7881
|
+
if (created) {
|
|
7882
|
+
console.log(`\u5DF2\u751F\u6210\u8986\u5199\u6A21\u677F: ${overwritePath}`);
|
|
7883
|
+
console.log(colors.gray(" \u6A21\u677F\u53EA\u5EFA\u597D socks5 \u8282\u70B9\u4E0E\u5206\u7EC4\uFF0C\u5206\u6D41\u89C4\u5219\u9700\u4F60\u586B\u5199\uFF08CLI \u65E0\u4ECE\u77E5\u9053\u4F60\u7684\u5185\u7F51\u57DF\u540D\uFF09"));
|
|
7884
|
+
console.log(colors.gray(" \u7F16\u8F91\u540E\u6267\u884C mihomo start \u751F\u6548"));
|
|
7885
|
+
} else {
|
|
7886
|
+
console.log(`\u8986\u5199\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u672A\u6539\u52A8: ${overwritePath}`);
|
|
7887
|
+
}
|
|
7888
|
+
console.log("");
|
|
7889
|
+
console.log(`\u542F\u52A8\u96A7\u9053: mihomo tunnel up ${name}`);
|
|
7890
|
+
if (created) {
|
|
7891
|
+
await restartToApply(args);
|
|
7892
|
+
}
|
|
7893
|
+
}
|
|
7894
|
+
async function tunnelUp(args) {
|
|
7895
|
+
const name = getTunnelNameArg(args);
|
|
7896
|
+
const targets = name ? [resolveTunnel(name)] : getTunnels();
|
|
7897
|
+
if (targets.length === 0) {
|
|
7898
|
+
throw new CliError("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053", { hint: ["\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"] });
|
|
7899
|
+
}
|
|
7900
|
+
for (const config of targets) {
|
|
7901
|
+
if (ensureTunnelOverwriteFile(config)) {
|
|
7902
|
+
console.log(colors.gray(`\u5DF2\u8865\u5EFA\u8986\u5199\u6A21\u677F: ${getTunnelOverwritePath(config.name)}`));
|
|
7903
|
+
}
|
|
7904
|
+
const result = await startTunnel(config.name, { startedBy: "manual" });
|
|
7905
|
+
if (result.alreadyRunning) {
|
|
7906
|
+
console.log(`${colors.gray("\u5DF2\u5728\u8FD0\u884C")} ${config.name} (PID ${result.pid})`);
|
|
7907
|
+
} else {
|
|
7908
|
+
console.log(`${colors.green("\u5DF2\u542F\u52A8\u96A7\u9053")} ${config.name} \xB7 ${config.host} \u2192 127.0.0.1:${config.port} (PID ${result.pid})`);
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
function tunnelDown(args) {
|
|
7913
|
+
const name = getTunnelNameArg(args);
|
|
7914
|
+
const targets = name ? [resolveTunnel(name)] : getTunnels();
|
|
7915
|
+
if (targets.length === 0) {
|
|
7916
|
+
throw new CliError("\u6CA1\u6709\u914D\u7F6E\u96A7\u9053");
|
|
7917
|
+
}
|
|
7918
|
+
let stopped = 0;
|
|
7919
|
+
for (const config of targets) {
|
|
7920
|
+
const result = stopTunnel(config.name);
|
|
7921
|
+
if (result.notRunning) {
|
|
7922
|
+
console.log(`${colors.yellow("\u4E0D\u5728\u8FD0\u884C")} ${config.name}`);
|
|
7923
|
+
} else {
|
|
7924
|
+
console.log(`${colors.green("\u5DF2\u505C\u6B62\u96A7\u9053")} ${config.name} (PID ${result.pid})`);
|
|
7925
|
+
stopped++;
|
|
7926
|
+
}
|
|
7927
|
+
}
|
|
7928
|
+
if (stopped > 0) {
|
|
7929
|
+
console.log("");
|
|
7930
|
+
console.log(colors.gray("\u6CE8\u610F: \u8986\u5199\u6587\u4EF6\u4ECD\u5728\uFF0C\u914D\u7F6E\u91CC\u7684\u96A7\u9053\u8282\u70B9\u73B0\u5728\u6307\u5411\u672A\u76D1\u542C\u7684\u7AEF\u53E3"));
|
|
7931
|
+
}
|
|
7932
|
+
}
|
|
7933
|
+
async function tunnelStatus(args) {
|
|
7934
|
+
const name = getTunnelNameArg(args);
|
|
7935
|
+
if (!name) {
|
|
7936
|
+
await printTunnelList();
|
|
7937
|
+
return;
|
|
7938
|
+
}
|
|
7939
|
+
const config = resolveTunnel(name);
|
|
7940
|
+
const status = await getTunnelStatus(config);
|
|
7941
|
+
console.log("");
|
|
7942
|
+
console.log(`${colors.gray("\u96A7\u9053: ")}${config.name}${config.auto ? colors.gray(" [auto]") : ""}`);
|
|
7943
|
+
console.log(`${colors.gray("\u51FA\u53E3: ")}${config.host} \u2192 127.0.0.1:${config.port}`);
|
|
7944
|
+
console.log(`${colors.gray("\u72B6\u6001: ")}${formatState(status)}`);
|
|
7945
|
+
if (status.started_by) {
|
|
7946
|
+
console.log(`${colors.gray("\u6765\u6E90: ")}${status.started_by === "auto" ? "\u968F start \u62C9\u8D77" : "\u624B\u52A8\u542F\u52A8"}`);
|
|
7947
|
+
}
|
|
7948
|
+
console.log("");
|
|
7949
|
+
if (status.state === "dead-port") {
|
|
7950
|
+
console.log(colors.yellow("ssh \u8FDB\u7A0B\u8FD8\u5728\uFF0C\u4F46\u7AEF\u53E3\u6CA1\u6709\u76D1\u542C\u2014\u2014\u5185\u7F51\u5206\u6D41\u5F53\u524D\u4E0D\u53EF\u7528"));
|
|
7951
|
+
console.log(`\u91CD\u542F\u96A7\u9053: mihomo tunnel down ${config.name} && mihomo tunnel up ${config.name}`);
|
|
7952
|
+
console.log(colors.gray(` \u65E5\u5FD7: ${getTunnelLogPath(config.name)}`));
|
|
7953
|
+
console.log("");
|
|
7954
|
+
} else if (status.state === "stopped") {
|
|
7955
|
+
console.log(`\u542F\u52A8\u96A7\u9053: mihomo tunnel up ${config.name}`);
|
|
7956
|
+
console.log("");
|
|
7957
|
+
}
|
|
7958
|
+
}
|
|
7959
|
+
async function tunnelRemove(args) {
|
|
7960
|
+
const name = requireTunnelNameArg(args, "\u7528\u6CD5: mihomo tunnel rm <\u540D\u5B57> [-y]");
|
|
7961
|
+
const config = resolveTunnel(name);
|
|
7962
|
+
const skipConfirm = hasFlag(args, "-y", "--yes");
|
|
7963
|
+
if (!skipConfirm) {
|
|
7964
|
+
if (!process.stdin.isTTY) {
|
|
7965
|
+
throw new CliError("\u5220\u9664\u96A7\u9053\u9700\u8981\u786E\u8BA4", {
|
|
7966
|
+
label: "\u5DF2\u53D6\u6D88",
|
|
7967
|
+
hint: [`\u8DF3\u8FC7\u786E\u8BA4: mihomo tunnel rm ${config.name} -y`]
|
|
7968
|
+
});
|
|
7969
|
+
}
|
|
7970
|
+
const confirmed = await confirmPrompt(`\u786E\u8BA4\u5220\u9664\u96A7\u9053 "${config.name}"?`);
|
|
7971
|
+
if (!confirmed) {
|
|
7972
|
+
console.log("\u5DF2\u53D6\u6D88");
|
|
7973
|
+
return;
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
stopTunnel(config.name);
|
|
7977
|
+
removeTunnel(config.name);
|
|
7978
|
+
console.log(`${colors.green("\u5DF2\u5220\u9664\u96A7\u9053")} ${config.name}`);
|
|
7979
|
+
console.log("");
|
|
7980
|
+
console.log(`\u8986\u5199\u6587\u4EF6\u672A\u5220\u9664: ${getTunnelOverwritePath(config.name)}`);
|
|
7981
|
+
console.log(colors.gray(" \u5B83\u4ECD\u4F1A\u5411\u914D\u7F6E\u6CE8\u5165\u6307\u5411\u8BE5\u7AEF\u53E3\u7684\u8282\u70B9\uFF0C\u5982\u4E0D\u518D\u9700\u8981\u8BF7\u81EA\u884C\u5220\u9664"));
|
|
7982
|
+
}
|
|
7983
|
+
function resolveTunnel(name) {
|
|
7984
|
+
const config = findTunnel(name);
|
|
7985
|
+
if (!config) {
|
|
7986
|
+
const tunnels = getTunnels();
|
|
7987
|
+
const suggestion = suggestSimilar(
|
|
7988
|
+
name,
|
|
7989
|
+
tunnels.map((t) => t.name)
|
|
7990
|
+
);
|
|
7991
|
+
throw new CliError(`\u672A\u627E\u5230\u96A7\u9053 "${name}"`, {
|
|
7992
|
+
hint: [
|
|
7993
|
+
...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [],
|
|
7994
|
+
...tunnels.length > 0 ? ["", "\u53EF\u7528\u96A7\u9053:", ...tunnels.map((t) => ` ${t.name}`)] : ["", "\u6DFB\u52A0\u96A7\u9053: mihomo tunnel add <\u540D\u5B57> --host <ssh\u4E3B\u673A> --port <\u7AEF\u53E3>"]
|
|
7995
|
+
]
|
|
7996
|
+
});
|
|
7997
|
+
}
|
|
7998
|
+
return config;
|
|
7999
|
+
}
|
|
8000
|
+
var SUBCOMMANDS5 = [
|
|
8001
|
+
{ name: "list", handler: printTunnelList },
|
|
8002
|
+
{ name: "add", handler: tunnelAdd },
|
|
8003
|
+
{ name: "up", aliases: ["start"], handler: tunnelUp },
|
|
8004
|
+
{ name: "down", aliases: ["stop"], handler: tunnelDown },
|
|
8005
|
+
{ name: "status", handler: tunnelStatus },
|
|
8006
|
+
{ name: "remove", aliases: ["rm", "delete"], handler: tunnelRemove }
|
|
8007
|
+
];
|
|
8008
|
+
async function cmdTunnel(args) {
|
|
8009
|
+
await dispatchSubcommand(args, SUBCOMMANDS5, {
|
|
8010
|
+
// 无 action → 列表;未知 action → 报错(不静默回落,否则 `tunnel upp` 会看似成功)
|
|
8011
|
+
fallback: printTunnelList,
|
|
8012
|
+
onUnknown: (action) => {
|
|
8013
|
+
const names = SUBCOMMANDS5.flatMap((c) => [c.name, ...c.aliases ?? []]);
|
|
8014
|
+
const suggestion = suggestSimilar(action, names);
|
|
8015
|
+
throw new CliError(`\u672A\u77E5\u7684 tunnel \u5B50\u547D\u4EE4: ${action}`, {
|
|
8016
|
+
hint: [...suggestion.length > 0 ? [`\u662F\u5426\u60F3\u8F93\u5165: ${suggestion.join(" / ")}?`] : [], "", "\u53EF\u7528\u5B50\u547D\u4EE4: list, add, up, down, status, rm"]
|
|
8017
|
+
});
|
|
8018
|
+
}
|
|
8019
|
+
});
|
|
8020
|
+
}
|
|
8021
|
+
|
|
7345
8022
|
// src/commands/ui.ts
|
|
7346
8023
|
function cmdUI(args) {
|
|
7347
8024
|
const uiName = args[1] || "zash";
|
|
@@ -7366,7 +8043,7 @@ function cmdUI(args) {
|
|
|
7366
8043
|
}
|
|
7367
8044
|
|
|
7368
8045
|
// src/commands/update.ts
|
|
7369
|
-
import { execFile, spawn as
|
|
8046
|
+
import { execFile, spawn as spawn4 } from "child_process";
|
|
7370
8047
|
import { promisify } from "util";
|
|
7371
8048
|
var execFileAsync = promisify(execFile);
|
|
7372
8049
|
var NPM_VIEW_TIMEOUT_MS = 15e3;
|
|
@@ -7397,7 +8074,7 @@ async function cmdUpdate() {
|
|
|
7397
8074
|
console.log("\u6B63\u5728\u66F4\u65B0 mihomo-cli...");
|
|
7398
8075
|
console.log("");
|
|
7399
8076
|
await new Promise((resolve, reject) => {
|
|
7400
|
-
const npm =
|
|
8077
|
+
const npm = spawn4("npm", ["install", "-g", PKG_NAME], { stdio: "inherit" });
|
|
7401
8078
|
npm.on("close", (code) => {
|
|
7402
8079
|
if (code === 0) {
|
|
7403
8080
|
resolve();
|
|
@@ -7573,6 +8250,20 @@ var COMMANDS = [
|
|
|
7573
8250
|
group: "system",
|
|
7574
8251
|
usage: ["daemon on|off \u5F00\u673A\u81EA\u542F + \u5D29\u6E83\u91CD\u542F\uFF08\u4EC5 Mixed\uFF0C\u9700\u7BA1\u7406\u5458\u5BC6\u7801\uFF09", "daemon status \u67E5\u770B\u4FDD\u6D3B\u72B6\u6001"]
|
|
7575
8252
|
},
|
|
8253
|
+
{
|
|
8254
|
+
// 别名不能用 tun——已被 TUN 模式快捷命令占用(注册表重复 token 会在模块加载期直接抛错)
|
|
8255
|
+
name: "tunnel",
|
|
8256
|
+
aliases: ["ssh", "tunnels"],
|
|
8257
|
+
handler: cmdTunnel,
|
|
8258
|
+
group: "system",
|
|
8259
|
+
usage: [
|
|
8260
|
+
"tunnel \u5217\u51FA ssh \u96A7\u9053\uFF08\u522B\u540D ssh\uFF09",
|
|
8261
|
+
"tunnel add <\u540D\u5B57> --host <\u4E3B\u673A> --port <\u7AEF\u53E3> [--no-auto]",
|
|
8262
|
+
"tunnel up|down [\u540D\u5B57] \u542F\u52A8/\u505C\u6B62\u96A7\u9053\uFF08\u65E0\u53C2\u5373\u5168\u90E8\uFF09",
|
|
8263
|
+
"tunnel status [\u540D\u5B57] \u67E5\u770B\u96A7\u9053\u72B6\u6001\uFF08\u771F\u5B9E\u63A2\u6D4B\u7AEF\u53E3\uFF09",
|
|
8264
|
+
"tunnel rm <\u540D\u5B57> [-y] \u5220\u9664\u96A7\u9053\uFF08\u4E0D\u5220\u8986\u5199\u6587\u4EF6\uFF09"
|
|
8265
|
+
]
|
|
8266
|
+
},
|
|
7576
8267
|
{
|
|
7577
8268
|
name: "update",
|
|
7578
8269
|
aliases: ["upd", "upgrade"],
|
|
@@ -7678,7 +8369,7 @@ async function main() {
|
|
|
7678
8369
|
if (args.length === 0) {
|
|
7679
8370
|
assertSupportedPlatform("status");
|
|
7680
8371
|
ensureDirs();
|
|
7681
|
-
printStatus();
|
|
8372
|
+
await printStatus();
|
|
7682
8373
|
printShortHelp();
|
|
7683
8374
|
return;
|
|
7684
8375
|
}
|