Make clash the primary CLI command

This commit is contained in:
Arvin
2026-07-30 17:25:15 +08:00
parent ef59c36521
commit 3be0915a71
12 changed files with 687 additions and 480 deletions
+88 -86
View File
@@ -20,7 +20,7 @@
- 🚀 **自动识别系统架构**:自动下载并使用对应 Clash 内核 - 🚀 **自动识别系统架构**:自动下载并使用对应 Clash 内核
- 🧪 **端口自动检测与分配**:避免冲突 - 🧪 **端口自动检测与分配**:避免冲突
- 🔄 **多订阅管理**:可以保存多个订阅,通过 `clashctl use` 切换当前主订阅。 - 🔄 **多订阅管理**:可以保存多个订阅,通过 `clash use` 切换当前主订阅。
- 💫 **节点选择**:使用编号交互选择策略组和节点。 - 💫 **节点选择**:使用编号交互选择策略组和节点。
- 🌐 **Tun 模式**:用于透明代理接管场景(需root方式安装)。 - 🌐 **Tun 模式**:用于透明代理接管场景(需root方式安装)。
- 🧠 **Mixin 机制**:可按需追加/覆盖 Clash 配置 - 🧠 **Mixin 机制**:可按需追加/覆盖 Clash 配置
@@ -56,55 +56,57 @@ bash install.sh
```bash ```bash
〽️ 常用命令 〽️ 常用命令
clashon 🚀 开启代理 clashon 🚀 开启代理
clashoff ⛔ 关闭代理 clashoff ⛔ 关闭代理
clashctl select 💫 选择节点 clash select 💫 选择节点
clashctl mode 🧭 查看或切换路由模式 clash mode 🧭 查看或切换路由模式
clashtest 🌐 测试当前节点访问 Google / YouTube clashtest 🌐 测试当前节点访问 Google / YouTube
🕹️ 控制台 🕹️ 控制台
clashui 🕹️ 查看 Web 控制台 clashui 🕹️ 查看 Web 控制台
📦 订阅 📦 订阅
clashctl add ➕ 添加订阅 clash add ➕ 添加订阅
clashctl add local ➕ 从 runtime/subscriptions 导入本地订阅 clash add local ➕ 从 runtime/subscriptions 导入本地订阅
clashctl use 💱 切换订阅 clash use 💱 切换订阅
clashctl ls 📜 查看订阅列表 clash ls 📜 查看订阅列表
🔐 密钥管理 🔐 密钥管理
clashctl secret 🔑 查看密钥 clash secret 🔑 查看密钥
clashctl secret 123 🔐 设置密钥 clash secret 123 🔐 设置密钥
📌 高级 📌 高级
clashctl lan 🏠 局域网代理管理 clash lan 🏠 局域网代理管理
clashctl tun 🧪 Tun 模式管理(需root方式安装) clash tun 🧪 Tun 模式管理(需root方式安装)
clashctl boot 🚦 开机代理接管管理 clash boot 🚦 开机代理接管管理
clashctl mixin 🧩 Mixin 配置管理 clash mixin 🧩 Mixin 配置管理
clashctl relay 🔗 多跳节点管理 clash relay 🔗 多跳节点管理
clashctl sub 🧩 订阅高级管理(启用 / 禁用 / 重命名 / 删除) clash sub 🧩 订阅高级管理(启用 / 禁用 / 重命名 / 删除)
clashctl upgrade 🚀 升级当前或指定内核 clash upgrade 🚀 升级当前或指定内核
clashctl update 🔄 更新项目代码 clash update 🔄 更新项目代码
📜 日志 📜 日志
clashctl doctor 🩺 诊断面板 clash doctor 🩺 诊断面板
clashctl log/logs 📜 查看日志 clash log/logs 📜 查看日志
💡 显示更多帮助命令:clashctl -h 💡 显示更多帮助命令:clash -h
``` ```
`clashctl` 旧入口继续保留兼容;新文档、帮助和终端提示统一使用更简洁的 `clash`。
------ ------
## 🧭 路由模式与节点连通性 ## 🧭 路由模式与节点连通性
```bash ```bash
clashctl mode clash mode
clashctl mode global clash mode global
clashctl mode rule clash mode rule
clashctl mode direct clash mode direct
clashctl test clash test
clashtest clashtest
clashtest "节点选择" clashtest "节点选择"
``` ```
- `clashctl mode` 查看当前的 Clash 路由模式,支持切换 `rule`、`global`、`direct`。 - `clash mode` 查看当前的 Clash 路由模式,支持切换 `rule`、`global`、`direct`。
- `global` 模式使用 `GLOBAL` 策略组当前选择的策略组或节点;如需调整可执行 `clashctl select`。 - `global` 模式使用 `GLOBAL` 策略组当前选择的策略组或节点;如需调整可执行 `clash select`。
- `clashctl test` 与 `clashtest` 等价,默认测试当前路由模式对应的策略组当前选择能否访问 Google 和 YouTube,并显示延迟。 - `clash test` 与 `clashtest` 等价,默认测试当前路由模式对应的策略组当前选择能否访问 Google 和 YouTube,并显示延迟。
- 连通性测试不会切换节点;任一目标不可访问时命令返回非零状态,便于脚本判断。 - 连通性测试不会切换节点;任一目标不可访问时命令返回非零状态,便于脚本判断。
------ ------
@@ -134,7 +136,7 @@ $ clashsecret
``` ```
- 可通过浏览器打开 `Web` 控制台进行可视化操作,例如切换节点、查看日志等。 - 可通过浏览器打开 `Web` 控制台进行可视化操作,例如切换节点、查看日志等。
- `clashctl secret` 与 `clashsecret` 都支持无参数查看、有参数直接设置。 - `clash secret` 与 `clashsecret` 都支持无参数查看、有参数直接设置。
- 默认使用 [zashboard](https://github.com/Zephyruso/zashboard) 作为控制台前端,如需更换可自行配置。 - 默认使用 [zashboard](https://github.com/Zephyruso/zashboard) 作为控制台前端,如需更换可自行配置。
- 控制台中的“浏览器出口 IP”由访问面板的浏览器发起查询,可能与远端 Mihomo 的代理出口不同;远端出口请在服务器上通过本地代理端口查询。 - 控制台中的“浏览器出口 IP”由访问面板的浏览器发起查询,可能与远端 Mihomo 的代理出口不同;远端出口请在服务器上通过本地代理端口查询。
- 运行配置会把 `external-ui` 指向本地 Dashboard 目录,并把 `external-ui-url` 指向 zashboard 的 `dist.zip` 下载地址;面板内 `/upgrade/ui` 会使用该地址更新前端。 - 运行配置会把 `external-ui` 指向本地 Dashboard 目录,并把 `external-ui-url` 指向 zashboard 的 `dist.zip` 下载地址;面板内 `/upgrade/ui` 会使用该地址更新前端。
@@ -147,32 +149,32 @@ $ clashsecret
项目默认开启局域网代理:运行配置会写入 `allow-lan: true`,并把 `external-controller` 绑定到 `0.0.0.0`,避免订阅文件里的 `allow-lan: false` 覆盖项目默认值。 项目默认开启局域网代理:运行配置会写入 `allow-lan: true`,并把 `external-controller` 绑定到 `0.0.0.0`,避免订阅文件里的 `allow-lan: false` 覆盖项目默认值。
```bash ```bash
clashctl lan status clash lan status
clashctl lan on clash lan on
clashctl lan off clash lan off
``` ```
开启后,同一局域网设备可把 HTTP / SOCKS 代理地址设置为 `http://<本机局域网IP>:<mixed-port>`,端口默认是 `7890`;如访问不了,请检查系统防火墙是否放行该端口。 开启后,同一局域网设备可把 HTTP / SOCKS 代理地址设置为 `http://<本机局域网IP>:<mixed-port>`,端口默认是 `7890`;如访问不了,请检查系统防火墙是否放行该端口。
------ ------
OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。 OpenWrt 下 root/system 安装会把 `clash`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。
## 🧰 常用管理命令 ## 🧰 常用管理命令
### 多订阅管理 ### 多订阅管理
``` ```
clashctl add <订阅链接> <名称> clash add <订阅链接> <名称>
clashctl use clash use
clashctl ls clash ls
clashctl sub update clash sub update
clashsub update # 兼容旧版命令 clashsub update # 兼容旧版命令
clashctl sub clash sub
clashctl sub list clash sub list
clashctl sub enable <名称> clash sub enable <名称>
clashctl sub disable <名称> clash sub disable <名称>
clashctl sub rename <旧名称> <新名称> clash sub rename <旧名称> <新名称>
clashctl sub remove <名称> clash sub remove <名称>
``` ```
WSL / 普通用户如果无权写入 `/etc/environment`,`clashon` 会自动降级:运行时照常启动,当前 Shell 代理变量生效;系统代理持久接管和开机代理保持不可用。 WSL / 普通用户如果无权写入 `/etc/environment`,`clashon` 会自动降级:运行时照常启动,当前 Shell 代理变量生效;系统代理持久接管和开机代理保持不可用。
@@ -182,20 +184,20 @@ WSL / 普通用户如果无权写入 `/etc/environment`,`clashon` 会自动降
推荐使用交互导入,放置目录为:`$PROJECT_DIR/runtime/subscriptions/` 推荐使用交互导入,放置目录为:`$PROJECT_DIR/runtime/subscriptions/`
```bash ```bash
clashctl add local clash add local
# 输入:clash.yaml # 输入:clash.yaml
``` ```
实际等价于: 实际等价于:
```bash ```bash
clashctl add "file://$PROJECT_DIR/runtime/subscriptions/clash.yaml" clash add "file://$PROJECT_DIR/runtime/subscriptions/clash.yaml"
``` ```
进阶用法:也可以直接使用 `file://` 绝对路径导入: 进阶用法:也可以直接使用 `file://` 绝对路径导入:
```bash ```bash
clashctl add "file:///绝对路径/clash.yaml" home clash add "file:///绝对路径/clash.yaml" home
``` ```
支持格式: 支持格式:
@@ -207,11 +209,11 @@ clashctl add "file:///绝对路径/clash.yaml" home
### 开机接管(内核 + 代理) ### 开机接管(内核 + 代理)
```bash ```bash
clashctl boot status clash boot status
clashctl boot on clash boot on
clashctl boot off clash boot off
clashctl boot runtime on|off|status clash boot runtime on|off|status
clashctl boot proxy on|off|status clash boot proxy on|off|status
``` ```
- `boot runtime`:只管理内核开机自启(systemd / systemd-user 可用,script 后端为 `unsupported`)。 - `boot runtime`:只管理内核开机自启(systemd / systemd-user 可用,script 后端为 `unsupported`)。
@@ -244,11 +246,11 @@ opkg install bash curl tar gzip coreutils-readlink unzip
```bash ```bash
bash install.sh bash install.sh
clashon clashon
clashctl status clash status
clashoff clashoff
``` ```
OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。 OpenWrt 下 root/system 安装会把 `clash`、`clashon`、`clashoff` 等命令入口写入 `/usr/bin`,运行状态、日志和内核二进制仍保存在项目目录的 `runtime/` 下。仅脚本模式不会注册开机自启,设备重启后需要重新执行 `clashon`。
## 🏗️ 架构设计架构简述 ## 🏗️ 架构设计架构简述
@@ -258,7 +260,7 @@ OpenWrt 下 root/system 安装会把 `clashctl`、`clashon`、`clashoff` 等命
用户入口层。 用户入口层。
- `clashctl` - `clash`
- `clashon` - `clashon`
- `clashoff` - `clashoff`
- `status` - `status`
@@ -341,13 +343,13 @@ CLASH_GH_PROXY=https://ghfast.top
``` ```
可用镜像列表参考:<https://ghproxy.link/> 可用镜像列表参考:<https://ghproxy.link/>
当前镜像使用状态可通过 `clashctl doctor` 查看。 当前镜像使用状态可通过 `clash doctor` 查看。
### 内置运行依赖 ### 内置运行依赖
当前正式支持的架构为 `amd64`、`arm64`、`armv7`。超出这三种架构时会明确失败,不会伪装成已支持。 当前正式支持的架构为 `amd64`、`arm64`、`armv7`。超出这三种架构时会明确失败,不会伪装成已支持。
如果安装环境访问 GitHub 很慢,可以把 Mihomo、yq、subconverter 的刚需文件跟随项目一起分发。安装和 `clashctl upgrade` 会优先读取 `resources/bin` 中与当前版本、架构对应的精确文件名;本地没有对应文件时,会回退到原来的远程下载逻辑,不影响后续升级内核。 如果安装环境访问 GitHub 很慢,可以把 Mihomo、yq、subconverter 的刚需文件跟随项目一起分发。安装和 `clash upgrade` 会优先读取 `resources/bin` 中与当前版本、架构对应的精确文件名;本地没有对应文件时,会回退到原来的远程下载逻辑,不影响后续升级内核。
Clash 仅作为兼容内核处理,固定走远程下载,不会命中 `resources/bin` 中的本地资源。 Clash 仅作为兼容内核处理,固定走远程下载,不会命中 `resources/bin` 中的本地资源。
@@ -381,19 +383,19 @@ resources/geo/Country.mmdb
查看当前模板: 查看当前模板:
```bash ```bash
clashctl mixin clash mixin
``` ```
编辑: 编辑:
```bash ```bash
clashctl mixin edit clash mixin edit
``` ```
查看最终运行配置: 查看最终运行配置:
```bash ```bash
clashctl mixin runtime clash mixin runtime
``` ```
### ###
@@ -403,10 +405,10 @@ clashctl mixin runtime
## 🔄 更新 ## 🔄 更新
```bash ```bash
clashctl update clash update
clashctl upgrade clash upgrade
clashctl upgrade mihomo clash upgrade mihomo
clashctl upgrade clash clash upgrade clash
``` ```
`update` 用于更新项目代码与运行依赖。`upgrade` 用于升级当前或指定代理内核。 `update` 用于更新项目代码与运行依赖。`upgrade` 用于升级当前或指定代理内核。
@@ -416,10 +418,10 @@ clashctl upgrade clash
## 🧩 Mixin 配置 ## 🧩 Mixin 配置
```bash ```bash
clashctl mixin clash mixin
clashctl mixin edit clash mixin edit
clashctl mixin raw clash mixin raw
clashctl mixin runtime clash mixin runtime
``` ```
Mixin 是运行配置补丁,不是订阅管理。它优先通过 `runtime/mixin.yaml`(兼容读取 `config/mixin.yaml`)对当前 active 订阅生成的运行配置执行: Mixin 是运行配置补丁,不是订阅管理。它优先通过 `runtime/mixin.yaml`(兼容读取 `config/mixin.yaml`)对当前 active 订阅生成的运行配置执行:
@@ -451,7 +453,7 @@ append:
编辑后执行: 编辑后执行:
```bash ```bash
clashctl mixin edit clash mixin edit
``` ```
它会重新生成配置;如果代理正在运行,会自动重启应用。 它会重新生成配置;如果代理正在运行,会自动重启应用。
@@ -468,8 +470,8 @@ clashui
按域名小范围测试: 按域名小范围测试:
```bash ```bash
clashctl relay add 多跳-示例 节点A 节点B --domain example.com clash relay add 多跳-示例 节点A 节点B --domain example.com
clashctl relay list clash relay list
``` ```
也可以使用快捷入口:`clashrelay list`。 也可以使用快捷入口:`clashrelay list`。
@@ -477,13 +479,13 @@ clashctl relay list
全局接管: 全局接管:
```bash ```bash
clashctl relay add 全局多跳 节点A 节点B --match clash relay add 全局多跳 节点A 节点B --match
``` ```
`--match` 会让所有未提前命中的流量走多跳,建议先用 `--domain` 验证链路。删除多跳配置: `--match` 会让所有未提前命中的流量走多跳,建议先用 `--domain` 验证链路。删除多跳配置:
```bash ```bash
clashctl relay remove 多跳-示例 clash relay remove 多跳-示例
``` ```
------ ------
@@ -491,22 +493,22 @@ clashctl relay remove 多跳-示例
## 🌐 Tun 模式 ## 🌐 Tun 模式
```bash ```bash
clashctl tun on clash tun on
clashctl tun off clash tun off
clashctl tun on-proxy-off clash tun on-proxy-off
clashctl tun off-proxy-on clash tun off-proxy-on
clashctl tun doctor clash tun doctor
clashctl tun logs clash tun logs
``` ```
Tun 用于透明接管链路。`tun on` 只负责开启 Tun,不会自动关闭系统代理;如需切换到 Tun 接管并关闭系统代理,使用 `clashctl tun on-proxy-off`。如需关闭 Tun 并恢复普通系统代理模式,使用 `clashctl tun off-proxy-on`。 Tun 用于透明接管链路。`tun on` 只负责开启 Tun,不会自动关闭系统代理;如需切换到 Tun 接管并关闭系统代理,使用 `clash tun on-proxy-off`。如需关闭 Tun 并恢复普通系统代理模式,使用 `clash tun off-proxy-on`。
`clashctl doctor` 会检查 Tun 与系统代理是否同时开启;如果同时开启,会提示 `clashctl tun on-proxy-off`,避免流量接管路径重复或排障混淆。 `clash doctor` 会检查 Tun 与系统代理是否同时开启;如果同时开启,会提示 `clash tun on-proxy-off`,避免流量接管路径重复或排障混淆。
`tun on` 是动作反馈,展示当前关键配置和简短状态;完整证据请看: `tun on` 是动作反馈,展示当前关键配置和简短状态;完整证据请看:
```bash ```bash
clashctl tun doctor clash tun doctor
``` ```
Tun 判断不会简单把 `root` 等同于拥有 `CAP_NET_ADMIN`,也不会把 main table 默认路由未切换直接等同于 Tun 未生效。诊断会结合运行后端、容器环境、进程能力、Tun adapter、policy routing、路由表和日志证据。 Tun 判断不会简单把 `root` 等同于拥有 `CAP_NET_ADMIN`,也不会把 main table 默认路由未切换直接等同于 Tun 未生效。诊断会结合运行后端、容器环境、进程能力、Tun adapter、policy routing、路由表和日志证据。
@@ -519,7 +521,7 @@ Tun 判断不会简单把 `root` 等同于拥有 `CAP_NET_ADMIN`,也不会把
bash uninstall.sh bash uninstall.sh
``` ```
默认执行完整卸载:停止 mihomo/subconverter,关闭系统代理持久接管,删除 systemd/脚本入口、`clashctl`、命令补全、shell alias、shell proxy 持久状态、controller secret 和运行目录。完整卸载完成后,脚本会提示其他卸载方式。 默认执行完整卸载:停止 mihomo/subconverter,关闭系统代理持久接管,删除 systemd/脚本入口、`clash`、命令补全、shell alias、shell proxy 持久状态、controller secret 和运行目录。完整卸载完成后,脚本会提示其他卸载方式。
`--purge-runtime` 保留为兼容别名;默认卸载已经会清理运行目录。 `--purge-runtime` 保留为兼容别名;默认卸载已经会清理运行目录。
+1 -1
View File
@@ -65,7 +65,7 @@ if [ -n "$(subscription_url 2>/dev/null || true)" ]; then
if [ -f "$RUNTIME_DIR/tmp/subscription-invalid-preview.txt" ]; then if [ -f "$RUNTIME_DIR/tmp/subscription-invalid-preview.txt" ]; then
echo "🧾 调试预览:$RUNTIME_DIR/tmp/subscription-invalid-preview.txt" echo "🧾 调试预览:$RUNTIME_DIR/tmp/subscription-invalid-preview.txt"
fi fi
echo "👉 下一步:clashctl doctor" echo "👉 下一步:clash doctor"
echo echo
exit 1 exit 1
fi fi
+1 -1
View File
@@ -1,6 +1,6 @@
# 内置运行依赖 # 内置运行依赖
如果安装时 GitHub 下载太慢,可以把 Mihomo、yq、subconverter 提前放到本目录。安装和 `clashctl upgrade` 会优先读取这里的文件;没有匹配文件时,仍会回退到原来的下载逻辑。 如果安装时 GitHub 下载太慢,可以把 Mihomo、yq、subconverter 提前放到本目录。安装和 `clash upgrade` 会优先读取这里的文件;没有匹配文件时,仍会回退到原来的下载逻辑。
Clash 是兼容内核,固定走远程下载,不使用本目录中的本地资源。 Clash 是兼容内核,固定走远程下载,不使用本目录中的本地资源。
+40 -30
View File
@@ -216,12 +216,12 @@ _clash_alias_unset_shell_proxy() {
} }
_clash_alias_status_next() { _clash_alias_status_next() {
_clashctl_real status-next 2>/dev/null || echo "clashctl status" _clashctl_real status-next 2>/dev/null || echo "clash status"
} }
_clash_alias_prepare_on() { _clash_alias_prepare_on() {
# 这里不直接自己做 regenerate / restart / fallback, # 这里不直接自己做 regenerate / restart / fallback,
# 而是统一交给 clashctl on 主链处理,避免双执行链。 # 而是统一交给 clash on 主链处理,避免双执行链。
# shell 层只负责“闭环体验”,不抢 runtime / build 的职责。 # shell 层只负责“闭环体验”,不抢 runtime / build 的职责。
return 0 return 0
} }
@@ -258,12 +258,12 @@ _clash_alias_run_on() {
if [ "$on_rc" -ne 0 ]; then if [ "$on_rc" -ne 0 ]; then
if _clash_alias_export_system_proxy; then if _clash_alias_export_system_proxy; then
echo "🚨 clashctl on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2 echo "🚨 clash on 返回非 0,但系统代理已写入,继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2 sed 's/^/ /' "$on_output" >&2
fi fi
elif _clash_alias_proxy_on_system; then elif _clash_alias_proxy_on_system; then
echo "🚨 clashctl on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2 echo "🚨 clash on 返回非 0,已通过 proxy on 继续同步当前 Shell(底层返回码:$on_rc)" >&2
if [ -s "$on_output" ]; then if [ -s "$on_output" ]; then
sed 's/^/ /' "$on_output" >&2 sed 's/^/ /' "$on_output" >&2
fi fi
@@ -304,7 +304,7 @@ _clash_alias_auto_restore_proxy() {
return 0 return 0
} }
clashctl() { _clash_command() {
case "${1:-}" in case "${1:-}" in
on) on)
shift || true shift || true
@@ -349,101 +349,111 @@ clashctl() {
esac esac
} }
# 快捷入口全部收敛到 clashctl 函数 clashctl() {
_clash_command "$@"
}
if [ "${CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED:-false}" = "true" ]; then
clash() {
_clash_command "$@"
}
fi
# 快捷入口全部收敛到统一命令函数
clashon() { clashon() {
clashctl on "$@" || return $? _clash_command on "$@" || return $?
} }
clashoff() { clashoff() {
clashctl off "$@" || return $? _clash_command off "$@" || return $?
} }
clashproxy() { clashproxy() {
case "${1:-show}" in case "${1:-show}" in
on) on)
clashctl proxy on _clash_command proxy on
;; ;;
off) off)
clashctl proxy off _clash_command proxy off
;; ;;
show|status) show|status)
clashctl proxy show _clash_command proxy show
;; ;;
groups) groups)
clashctl proxy groups _clash_command proxy groups
;; ;;
current) current)
shift || true shift || true
clashctl proxy current "$@" _clash_command proxy current "$@"
;; ;;
nodes) nodes)
shift || true shift || true
clashctl proxy nodes "$@" _clash_command proxy nodes "$@"
;; ;;
select) select)
shift || true shift || true
clashctl proxy select "$@" _clash_command proxy select "$@"
;; ;;
*) *)
echo "📜 用法:clashproxy [show|on|off|groups|current|nodes|select]" echo "📜 用法:clashproxy [show|on|off|groups|current|nodes|select]"
echo "💡 主路径切节点请使用:clashselect 或 clashctl select" echo "💡 主路径切节点请使用:clashselect 或 clash select"
return 2 return 2
;; ;;
esac esac
} }
clashls() { clashls() {
clashctl ls "$@" _clash_command ls "$@"
} }
clashsub() { clashsub() {
clashctl sub "$@" _clash_command sub "$@"
} }
clashselect() { clashselect() {
clashctl select "$@" _clash_command select "$@"
} }
clashtest() { clashtest() {
clashctl test "$@" _clash_command test "$@"
} }
clashui() { clashui() {
clashctl ui "$@" _clash_command ui "$@"
} }
clashsecret() { clashsecret() {
clashctl secret "$@" _clash_command secret "$@"
} }
clashtun() { clashtun() {
clashctl tun "$@" _clash_command tun "$@"
} }
clashrelay() { clashrelay() {
clashctl relay "$@" _clash_command relay "$@"
} }
clashupgrade() { clashupgrade() {
clashctl upgrade "$@" _clash_command upgrade "$@"
} }
clashmixin() { clashmixin() {
case "${1:-}" in case "${1:-}" in
-e|--edit) -e|--edit)
clashctl mixin edit _clash_command mixin edit
;; ;;
-c|--raw) -c|--raw)
clashctl mixin raw _clash_command mixin raw
;; ;;
-r|--runtime) -r|--runtime)
clashctl mixin runtime _clash_command mixin runtime
;; ;;
"") "")
clashctl mixin _clash_command mixin
;; ;;
*) *)
clashctl mixin "$@" _clash_command mixin "$@"
;; ;;
esac esac
} }
+320 -314
View File
File diff suppressed because it is too large Load Diff
+75 -18
View File
@@ -243,23 +243,23 @@ install_build_result_text() {
install_next_step_text() { install_next_step_text() {
case "$(install_state_text)" in case "$(install_state_text)" in
ready) ready)
echo "clashctl select" echo "clash select"
;; ;;
verifying) verifying)
if install_has_subscription; then if install_has_subscription; then
echo "clashon" echo "clashon"
else else
echo "clashctl add <订阅链接>" echo "clash add <订阅链接>"
fi fi
;; ;;
broken) broken)
echo "clashctl doctor" echo "clash doctor"
;; ;;
stopped) stopped)
echo "clashctl add <订阅链接>" echo "clash add <订阅链接>"
;; ;;
*) *)
echo "clashctl status" echo "clash status"
;; ;;
esac esac
} }
@@ -951,7 +951,7 @@ download_file() {
rm -f "$fetch_tmp" 2>/dev/null || true rm -f "$fetch_tmp" 2>/dev/null || true
die_state "下载失败:${asset_name}" \ die_state "下载失败:${asset_name}" \
"请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clashctl doctor" "请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clash doctor"
fi fi
ordered_entries="$( ordered_entries="$(
@@ -1012,7 +1012,7 @@ EOF
rm -f "$fetch_tmp" 2>/dev/null || true rm -f "$fetch_tmp" 2>/dev/null || true
die_state "下载失败:${asset_name}" \ die_state "下载失败:${asset_name}" \
"请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clashctl doctor" "请检查网络连通性,或在 .env 中配置下载源后重试;也可先执行 clash doctor"
} }
download_text_tmp_file() { download_text_tmp_file() {
@@ -2103,7 +2103,7 @@ guard_sudo_on_user_install() {
echo "❗ 操作被拒绝:user 安装模式不支持以 sudo 运行" echo "❗ 操作被拒绝:user 安装模式不支持以 sudo 运行"
echo "🚨 原因:sudo 会将 runtime 文件写成 root:root,导致 systemctl --user 无法访问" echo "🚨 原因:sudo 会将 runtime 文件写成 root:root,导致 systemctl --user 无法访问"
echo "👤 请以安装用户(${SUDO_USER})直接执行:" echo "👤 请以安装用户(${SUDO_USER})直接执行:"
echo " clashctl tun ${_action}" echo " clash tun ${_action}"
echo echo
return 1 return 1
} }
@@ -2535,6 +2535,51 @@ clashctl_bin_entry_target() {
echo "$(command_install_dir)/clashctl-bin" echo "$(command_install_dir)/clashctl-bin"
} }
clash_entry_target() {
echo "$(command_install_dir)/clash"
}
clash_entry_marker() {
echo "# clash-for-linux managed command: clash"
}
clash_entry_is_managed() {
local target="${1:-}"
[ -n "${target:-}" ] || target="$(clash_entry_target)"
[ -f "$target" ] || return 1
grep -Fqx "$(clash_entry_marker)" "$target" 2>/dev/null
}
install_clash_entry() {
local target bin_target existing_command=""
target="$(clash_entry_target)"
bin_target="$(clashctl_bin_entry_target)"
if [ -e "$target" ] || [ -L "$target" ]; then
if ! clash_entry_is_managed "$target"; then
existing_command="$target"
warn "检测到已有 clash 命令,未覆盖:$existing_command"
warn "本项目仍可使用 clashctl;如需 clash 入口,请先自行处理同名命令"
return 0
fi
elif command -v clash >/dev/null 2>&1; then
existing_command="$(command -v clash 2>/dev/null || true)"
warn "检测到已有 clash 命令,未覆盖:$existing_command"
warn "本项目仍可使用 clashctl;如需 clash 入口,请先自行处理同名命令"
return 0
fi
cat > "$target" <<EOF
#!/usr/bin/env bash
$(clash_entry_marker)
set -euo pipefail
exec "$bin_target" "\$@"
EOF
chmod +x "$target"
}
ensure_command_install_dir_in_shell_path() { ensure_command_install_dir_in_shell_path() {
local install_dir shell_rc home_dir local install_dir shell_rc home_dir
@@ -2582,6 +2627,7 @@ exec "$bin_target" "\$@"
EOF EOF
chmod +x "$target" chmod +x "$target"
install_clash_entry
ensure_command_install_dir_in_shell_path ensure_command_install_dir_in_shell_path
} }
@@ -2635,7 +2681,7 @@ cleanup_legacy_shell_entries() {
} }
install_shell_alias_entry() { install_shell_alias_entry() {
local profile_file alias_file shell_rc bash_completion_file zsh_completion_file home_dir system_zsh_rc local profile_file alias_file shell_rc bash_completion_file zsh_completion_file home_dir system_zsh_rc clash_entry_managed
cleanup_legacy_shell_entries cleanup_legacy_shell_entries
@@ -2643,6 +2689,8 @@ install_shell_alias_entry() {
alias_file="$(alias_source_file)" alias_file="$(alias_source_file)"
bash_completion_file="$(bash_completion_entry_file)" bash_completion_file="$(bash_completion_entry_file)"
zsh_completion_file="$(zsh_completion_entry_file)" zsh_completion_file="$(zsh_completion_entry_file)"
clash_entry_managed="false"
clash_entry_is_managed && clash_entry_managed="true"
mkdir -p "$(dirname "$profile_file")" mkdir -p "$(dirname "$profile_file")"
[ -f "$alias_file" ] || die "未找到 alias 脚本:$alias_file" [ -f "$alias_file" ] || die "未找到 alias 脚本:$alias_file"
@@ -2651,6 +2699,7 @@ cat > "$profile_file" <<EOF
#!/usr/bin/env bash #!/usr/bin/env bash
# clash-for-linux shell entry # clash-for-linux shell entry
export PATH="$(command_install_dir):\$PATH" export PATH="$(command_install_dir):\$PATH"
export CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED="$clash_entry_managed"
if [ -z "\${CLASH_FOR_LINUX_SHELL_LOADED:-}" ] \ if [ -z "\${CLASH_FOR_LINUX_SHELL_LOADED:-}" ] \
&& [ -r "$alias_file" ] \ && [ -r "$alias_file" ] \
@@ -2705,6 +2754,9 @@ install_clashctl_completion() {
} }
remove_clashctl_entry() { remove_clashctl_entry() {
if clash_entry_is_managed; then
rm -f "$(clash_entry_target)" 2>/dev/null || true
fi
rm -f "$(clashctl_entry_target)" "$(clashctl_bin_entry_target)" 2>/dev/null || true rm -f "$(clashctl_entry_target)" "$(clashctl_bin_entry_target)" 2>/dev/null || true
remove_alias_command_wrappers remove_alias_command_wrappers
} }
@@ -3054,31 +3106,31 @@ install_status_label() {
install_default_next_action() { install_default_next_action() {
case "$(install_status_text)" in case "$(install_status_text)" in
ready) ready)
echo "clashctl select" echo "clash select"
;; ;;
stopped) stopped)
if install_has_subscription; then if install_has_subscription; then
echo "clashon" echo "clashon"
else else
echo "clashctl add <订阅链接>" echo "clash add <订阅链接>"
fi fi
;; ;;
verifying) verifying)
if status_is_running 2>/dev/null; then if status_is_running 2>/dev/null; then
echo "clashctl status" echo "clash status"
else else
echo "clashon" echo "clashon"
fi fi
;; ;;
broken) broken)
if install_has_subscription; then if install_has_subscription; then
echo "clashctl doctor" echo "clash doctor"
else else
echo "clashctl add <订阅链接>" echo "clash add <订阅链接>"
fi fi
;; ;;
*) *)
echo "clashctl status" echo "clash status"
;; ;;
esac esac
} }
@@ -3202,6 +3254,11 @@ print_install_summary() {
echo "💻 环境模式:${env_mode_text:-unknown}" echo "💻 环境模式:${env_mode_text:-unknown}"
echo "📁 安装路径:$project_path" echo "📁 安装路径:$project_path"
echo "👤 安装方式:${install_actor} / ${install_scope_text:-unknown}" echo "👤 安装方式:${install_actor} / ${install_scope_text:-unknown}"
if clash_entry_is_managed; then
echo "⌨️ 主命令:clash"
else
echo "⚠️ 主命令:clash 同名冲突,未由本项目接管(兼容入口 clashctl 仍可用)"
fi
echo "🔧 运行后端:${backend_text:-unknown}" echo "🔧 运行后端:${backend_text:-unknown}"
echo "📦 订阅:$subscription_text" echo "📦 订阅:$subscription_text"
[ -n "${node_count:-}" ] && echo "🔢 节点数量:$node_count" [ -n "${node_count:-}" ] && echo "🔢 节点数量:$node_count"
@@ -3211,8 +3268,8 @@ print_install_summary() {
echo "🚫 安装后验证:代理端口绑定被系统拒绝(非端口冲突)" echo "🚫 安装后验证:代理端口绑定被系统拒绝(非端口冲突)"
echo "📌 这通常是环境限制,修改端口通常无法解决" echo "📌 这通常是环境限制,修改端口通常无法解决"
install_mixed_port_bind_observation_line 2>/dev/null || true install_mixed_port_bind_observation_line 2>/dev/null || true
echo "👉 下一步:clashctl doctor" echo "👉 下一步:clash doctor"
echo "👉 日志:clashctl logs mihomo" echo "👉 日志:clash logs mihomo"
;; ;;
address_in_use) address_in_use)
: :
@@ -3225,5 +3282,5 @@ print_install_summary() {
echo echo
echo "💡 clashon / clashoff 为 shell 快捷入口;新终端会自动生效" echo "💡 clashon / clashoff 为 shell 快捷入口;新终端会自动生效"
echo "💡 当前终端若暂不可用,请先使用 clashctl on / clashctl off" echo "💡 当前终端若暂不可用,请先使用 clash on / clash off"
} }
+5 -2
View File
@@ -360,7 +360,7 @@ _clash_for_linux_complete_command() {
cur="${COMP_WORDS[COMP_CWORD]:-}" cur="${COMP_WORDS[COMP_CWORD]:-}"
case "$root" in case "$root" in
clashctl) clash|clashctl)
if [ "$COMP_CWORD" -eq 1 ]; then if [ "$COMP_CWORD" -eq 1 ]; then
_clash_for_linux_complete_top_level "$cur" _clash_for_linux_complete_top_level "$cur"
return 0 return 0
@@ -441,6 +441,9 @@ _clash_for_linux_complete_command() {
esac esac
} }
if [ "${CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED:-false}" = "true" ]; then
complete -F _clash_for_linux_complete_command clash
fi
complete -F _clash_for_linux_complete_command clashctl complete -F _clash_for_linux_complete_command clashctl
complete -F _clash_for_linux_complete_command clashtest complete -F _clash_for_linux_complete_command clashtest
complete -F _clash_for_linux_complete_command clashsub complete -F _clash_for_linux_complete_command clashsub
@@ -477,7 +480,7 @@ cmd_completion() {
completion_emit_zsh_script completion_emit_zsh_script
;; ;;
*) *)
die_usage "completion 参数不合法" "clashctl completion bash|zsh" die_usage "completion 参数不合法" "clash completion bash|zsh"
;; ;;
esac esac
} }
+20 -20
View File
@@ -119,7 +119,7 @@ http_code : $http_code"
detail="${detail} detail="${detail}
process_status : $process_status" process_status : $process_status"
detail="${detail} detail="${detail}
next_step : clashctl doctor;查看 subconverter_log 定位转换失败原因" next_step : clash doctor;查看 subconverter_log 定位转换失败原因"
SUBCONVERTER_LAST_ERROR_DETAIL="$detail" SUBCONVERTER_LAST_ERROR_DETAIL="$detail"
SUBCONVERTER_LAST_ERROR_SUMMARY="subconverter 转换失败:$failure_type" SUBCONVERTER_LAST_ERROR_SUMMARY="subconverter 转换失败:$failure_type"
@@ -1165,8 +1165,8 @@ print_subscription_health_one() {
local name="$1" local name="$1"
local status fail_count last_success last_failure last_error auto_disabled auto_disabled_at enabled_text type_text url_text local status fail_count last_success last_failure last_error auto_disabled auto_disabled_at enabled_text type_text url_text
[ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clashctl health <订阅名称>" [ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clash health <订阅名称>"
subscription_exists "$name" || die_missing "订阅 $name" "clashctl ls" subscription_exists "$name" || die_missing "订阅 $name" "clash ls"
status="$(read_subscription_health_value "$name" "STATUS" 2>/dev/null || echo "unknown")" status="$(read_subscription_health_value "$name" "STATUS" 2>/dev/null || echo "unknown")"
fail_count="$(read_subscription_health_value "$name" "FAIL_COUNT" 2>/dev/null || echo "0")" fail_count="$(read_subscription_health_value "$name" "FAIL_COUNT" 2>/dev/null || echo "0")"
@@ -1310,7 +1310,7 @@ set_subscription_enabled() {
file="$(subscriptions_file)" file="$(subscriptions_file)"
ensure_subscriptions_file ensure_subscriptions_file
subscription_exists "$name" || die_missing "订阅 $name" "clashctl ls" subscription_exists "$name" || die_missing "订阅 $name" "clash ls"
case "$enabled" in case "$enabled" in
true|false) ;; true|false) ;;
@@ -1417,7 +1417,7 @@ print_build_explain() {
esac esac
echo echo
ui_next "clashctl status --verbose" ui_next "clash status --verbose"
ui_blank ui_blank
} }
@@ -1519,16 +1519,16 @@ build_explain_next_steps() {
failed_csv="$(build_failed_active_sources_csv)" failed_csv="$(build_failed_active_sources_csv)"
if [ "${last_status:-}" = "success" ]; then if [ "${last_status:-}" = "success" ]; then
echo "👉 clashctl health" echo "👉 clash health"
echo "👉 clashctl select" echo "👉 clash select"
return 0 return 0
fi fi
if [ -n "${failed_csv:-}" ]; then if [ -n "${failed_csv:-}" ]; then
echo "👉 clashctl health" echo "👉 clash health"
fi fi
echo "👉 clashctl doctor" echo "👉 clash doctor"
} }
runtime_config_file() { runtime_config_file() {
@@ -2484,7 +2484,7 @@ set_active_subscription() {
exists="$("$(yq_bin)" eval ".sources | has(\"$name\")" "$file")" exists="$("$(yq_bin)" eval ".sources | has(\"$name\")" "$file")"
[ "$exists" = "true" ] || die_missing "订阅 $name" "clashctl ls" [ "$exists" = "true" ] || die_missing "订阅 $name" "clash ls"
NAME="$name" "$(yq_bin)" eval -i ' NAME="$name" "$(yq_bin)" eval -i '
.active = env(NAME) .active = env(NAME)
@@ -2500,8 +2500,8 @@ remove_subscription() {
file="$(subscriptions_file)" file="$(subscriptions_file)"
ensure_subscriptions_file ensure_subscriptions_file
[ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clashctl health <订阅名称>" [ -n "${name:-}" ] || die_usage "订阅名称不能为空" "clash health <订阅名称>"
subscription_exists "$name" || die_missing "订阅 $name" "clashctl ls" subscription_exists "$name" || die_missing "订阅 $name" "clash ls"
if [ "$name" = "default" ]; then if [ "$name" = "default" ]; then
die "默认订阅不允许删除:default" die "默认订阅不允许删除:default"
@@ -2541,8 +2541,8 @@ rename_subscription() {
[ -n "${old_name:-}" ] || die "原订阅名称不能为空" [ -n "${old_name:-}" ] || die "原订阅名称不能为空"
[ -n "${new_name:-}" ] || die "新订阅名称不能为空" [ -n "${new_name:-}" ] || die "新订阅名称不能为空"
subscription_exists "$old_name" || die_missing "订阅 $old_name" "clashctl ls" subscription_exists "$old_name" || die_missing "订阅 $old_name" "clash ls"
subscription_exists "$new_name" && die_state "目标订阅已存在:$new_name" "clashctl ls" subscription_exists "$new_name" && die_state "目标订阅已存在:$new_name" "clash ls"
case "$new_name" in case "$new_name" in
*[!A-Za-z0-9._-]*) *[!A-Za-z0-9._-]*)
@@ -3039,7 +3039,7 @@ subscription_list_overview_lines() {
} }
subscription_list_recommendation_lines() { subscription_list_recommendation_lines() {
echo "👉 clashctl use 切换当前使用的订阅" echo "👉 clash use 切换当前使用的订阅"
} }
detect_subscription_format() { detect_subscription_format() {
@@ -3076,7 +3076,7 @@ prompt_subscription_if_needed() {
if ! subscription_url_is_supported "$input_url"; then if ! subscription_url_is_supported "$input_url"; then
ui_warn "订阅链接格式不合法,已跳过" ui_warn "订阅链接格式不合法,已跳过"
ui_next "稍后执行:clashctl add <订阅链接>" ui_next "稍后执行:clash add <订阅链接>"
return 0 return 0
fi fi
@@ -3221,7 +3221,7 @@ start_subconverter() {
fi fi
error "subconverter 启动失败" error "subconverter 启动失败"
warn "日志:$log_file" warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常" warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
return 1 return 1
} }
@@ -3618,7 +3618,7 @@ convert_subscription_via_subconverter() {
"${http_code:-unknown}" \ "${http_code:-unknown}" \
"$process_status" "$process_status"
warn "日志:$log_file" warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常" warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
rm -f "$tmp_file" "$curl_error_file" 2>/dev/null || true rm -f "$tmp_file" "$curl_error_file" 2>/dev/null || true
return 1 return 1
fi fi
@@ -3638,7 +3638,7 @@ convert_subscription_via_subconverter() {
"$process_status" "$process_status"
warn "订阅转换失败:subconverter 返回内容为空" warn "订阅转换失败:subconverter 返回内容为空"
warn "日志:$log_file" warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常" warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
rm -f "$tmp_file" 2>/dev/null || true rm -f "$tmp_file" 2>/dev/null || true
return 1 return 1
} }
@@ -3675,7 +3675,7 @@ convert_subscription_via_subconverter() {
warn "订阅转换失败:subconverter 返回内容不是合法 Clash YAML" warn "订阅转换失败:subconverter 返回内容不是合法 Clash YAML"
fi fi
warn "日志:$log_file" warn "日志:$log_file"
warn "下一步:请运行 clashctl doctor,或查看上述日志定位 subconverter 异常" warn "下一步:请运行 clash doctor,或查看上述日志定位 subconverter 异常"
rm -f "$tmp_file" 2>/dev/null || true rm -f "$tmp_file" 2>/dev/null || true
return 1 return 1
+1 -1
View File
@@ -204,7 +204,7 @@ update_project_code() {
fi fi
if git_has_local_changes && [ "$force_mode" != "true" ]; then if git_has_local_changes && [ "$force_mode" != "true" ]; then
die "检测到本地有未提交改动,请先提交,或使用 clashctl update --force 强制覆盖" die "检测到本地有未提交改动,请先提交,或使用 clash update --force 强制覆盖"
fi fi
info "正在获取最新代码" info "正在获取最新代码"
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=../core/common.sh
source "$PROJECT_DIR/scripts/core/common.sh"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
fixture_bin="$tmp_dir/bin"
mkdir -p "$fixture_bin"
command_install_dir() { printf '%s\n' "$fixture_bin"; }
ensure_command_install_dir_in_shell_path() { :; }
remove_alias_command_wrappers() { :; }
install_clashctl_entry
for command_name in clash clashctl clashctl-bin; do
if [ ! -x "$fixture_bin/$command_name" ]; then
echo "not ok - install should create $command_name" >&2
exit 1
fi
done
primary_help="$("$fixture_bin/clash" --help)"
compat_help="$("$fixture_bin/clashctl" --help)"
if ! printf '%s\n' "$primary_help" | grep -Fq "clash <command>"; then
echo "not ok - clash help should use the primary command name" >&2
exit 1
fi
if [ "$compat_help" != "$primary_help" ]; then
echo "not ok - clashctl should remain compatible with clash" >&2
exit 1
fi
completion_output="$(
"$fixture_bin/clash" completion bash
)"
if ! printf '%s\n' "$completion_output" | grep -Eq '^[[:space:]]*complete -F _clash_for_linux_complete_command clash$'; then
echo "not ok - completion should register the clash command" >&2
exit 1
fi
primary_completion="$(
bash --noprofile --norc -c '
CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED=true
source "$1"
COMP_WORDS=(clash st)
COMP_CWORD=1
_clash_for_linux_complete_command
printf "%s\n" "${COMPREPLY[@]}"
' bash <(printf '%s\n' "$completion_output")
)"
if ! printf '%s\n' "$primary_completion" | grep -Fxq status; then
echo "not ok - clash should complete top-level commands" >&2
exit 1
fi
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "%s\n" "$*"' > "$fixture_bin/clashctl-bin"
chmod +x "$fixture_bin/clashctl-bin"
shell_output="$(
PATH="$fixture_bin:/usr/bin:/bin" \
CLASH_FOR_LINUX_CLASH_ENTRY_MANAGED=true \
bash --noprofile --norc -c \
'source "$1"; clash status' \
bash "$PROJECT_DIR/scripts/core/alias.sh"
)"
if [ "$shell_output" != "status" ]; then
echo "not ok - clash shell function should delegate to the shared command path" >&2
printf 'actual: %s\n' "$shell_output" >&2
exit 1
fi
remove_clashctl_entry
for command_name in clash clashctl clashctl-bin; do
if [ -e "$fixture_bin/$command_name" ]; then
echo "not ok - uninstall should remove managed $command_name" >&2
exit 1
fi
done
printf '%s\n' '#!/usr/bin/env bash' 'echo foreign-clash' > "$fixture_bin/clash"
chmod +x "$fixture_bin/clash"
install_clashctl_entry >/dev/null 2>&1
if [ "$("$fixture_bin/clash")" != "foreign-clash" ]; then
echo "not ok - install should not overwrite an existing foreign clash command" >&2
exit 1
fi
remove_clashctl_entry
if [ "$("$fixture_bin/clash")" != "foreign-clash" ]; then
echo "not ok - uninstall should preserve an existing foreign clash command" >&2
exit 1
fi
other_bin="$tmp_dir/other-bin"
foreign_bin="$tmp_dir/foreign-bin"
mkdir -p "$other_bin" "$foreign_bin"
printf '%s\n' '#!/usr/bin/env bash' 'echo foreign-path-clash' > "$foreign_bin/clash"
chmod +x "$foreign_bin/clash"
command_install_dir() { printf '%s\n' "$other_bin"; }
PATH="$foreign_bin:/usr/bin:/bin" install_clashctl_entry >/dev/null 2>&1
if [ -e "$other_bin/clash" ]; then
echo "not ok - install should not shadow a foreign clash command elsewhere in PATH" >&2
exit 1
fi
remove_clashctl_entry
echo "ok - clash is primary, clashctl stays compatible, and foreign commands are preserved"
+1 -1
View File
@@ -41,7 +41,7 @@ run_help_case() {
return 1 return 1
fi fi
if ! printf '%s\n' "$output" | grep -Fq "clashctl select <策略组> <节点>"; then if ! printf '%s\n' "$output" | grep -Fq "clash select <策略组> <节点>"; then
echo "not ok - select help missing direct usage" >&2 echo "not ok - select help missing direct usage" >&2
printf '%s\n' "$output" >&2 printf '%s\n' "$output" >&2
return 1 return 1
+7 -6
View File
@@ -302,8 +302,9 @@ wait_install_runtime_ready() {
write_runtime_value "INSTALL_VERIFY_CONTROLLER_READY" "$controller_ready" write_runtime_value "INSTALL_VERIFY_CONTROLLER_READY" "$controller_ready"
} }
clashctl_command_available() { clash_command_available() {
[ -x "$(clashctl_entry_target)" ] \ clash_entry_is_managed \
|| [ -x "$(clashctl_entry_target)" ] \
|| [ -x "$(clashctl_bin_entry_target)" ] \ || [ -x "$(clashctl_bin_entry_target)" ] \
|| command -v clashctl >/dev/null 2>&1 || command -v clashctl >/dev/null 2>&1
} }
@@ -314,12 +315,12 @@ post_install_verify() {
local controller_ready="false" local controller_ready="false"
local bind_failure_kind="" bind_failure_port="" local bind_failure_kind="" bind_failure_port=""
if clashctl_command_available; then if clash_command_available; then
write_runtime_value "INSTALL_VERIFY_COMMAND_READY" "true" write_runtime_value "INSTALL_VERIFY_COMMAND_READY" "true"
else else
write_runtime_value "INSTALL_VERIFY_COMMAND_READY" "false" write_runtime_value "INSTALL_VERIFY_COMMAND_READY" "false"
write_runtime_event_value "RUNTIME_LAST_INSTALL_READY" "false" write_runtime_event_value "RUNTIME_LAST_INSTALL_READY" "false"
die "clashctl 安装失败,命令不可用" die "clash 管理命令安装失败,命令不可用"
fi fi
if [ -n "$(subscription_url 2>/dev/null || true)" ]; then if [ -n "$(subscription_url 2>/dev/null || true)" ]; then
@@ -357,8 +358,8 @@ post_install_verify() {
echo "🚫 安装后验证发现:代理端口 ${bind_failure_port:-unknown} 绑定被系统拒绝(非端口冲突)" echo "🚫 安装后验证发现:代理端口 ${bind_failure_port:-unknown} 绑定被系统拒绝(非端口冲突)"
echo "📌 修改端口通常无法解决;install 阶段不会通过自动换端口处理该问题" echo "📌 修改端口通常无法解决;install 阶段不会通过自动换端口处理该问题"
install_mixed_port_bind_observation_line 2>/dev/null || true install_mixed_port_bind_observation_line 2>/dev/null || true
echo "👉 下一步:clashctl doctor" echo "👉 下一步:clash doctor"
echo "👉 日志:clashctl logs mihomo" echo "👉 日志:clash logs mihomo"
echo echo
fi fi