proxyctl 0.3.2__tar.gz → 0.3.3__tar.gz
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.
- {proxyctl-0.3.2 → proxyctl-0.3.3}/PKG-INFO +1 -1
- {proxyctl-0.3.2 → proxyctl-0.3.3}/man/proxyctl.1 +5 -2
- {proxyctl-0.3.2 → proxyctl-0.3.3}/pyproject.toml +1 -1
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/cli.py +2 -0
- proxyctl-0.3.3/src/proxyctl/completion.py +381 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/explain.py +161 -20
- proxyctl-0.3.2/src/proxyctl/completion.py +0 -223
- {proxyctl-0.3.2 → proxyctl-0.3.3}/.gitignore +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/LICENSE +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/README.md +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/_io.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/check.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/status.py +0 -0
- {proxyctl-0.3.2 → proxyctl-0.3.3}/src/proxyctl/trace.py +0 -0
|
@@ -53,8 +53,10 @@ dns-lock / dns-unlock。
|
|
|
53
53
|
.SH COMMANDS
|
|
54
54
|
.SS 自描述(Agent 入口)
|
|
55
55
|
.TP
|
|
56
|
-
|
|
56
|
+
\fBproxyctl agent-guide\fR [\fB--list-sections\fR | \fB--section\fR \fINAME\fR]
|
|
57
57
|
输出给 LLM 的运行时入门 markdown(能力地图 / 引导路径 / 决策树 / envelope 字段表 / 锁文件位置 / footgun)。
|
|
58
|
+
v0.3.3 新增:\fB--list-sections\fR 列出所有 ASCII slug 形式的 section 名;
|
|
59
|
+
\fB--section <name>\fR 只输出该 section(模糊匹配 + did-you-mean),agent 按需取小块。
|
|
58
60
|
.TP
|
|
59
61
|
\fBproxyctl explain\fR [\fITOPIC\fR]
|
|
60
62
|
无参输出"想改 X 去哪?"速查表;带 topic 输出卡片。
|
|
@@ -74,7 +76,8 @@ supports_dry_run / needs_sudo / interactive / exit_codes / examples。
|
|
|
74
76
|
.B proxyctl doctor [--json]
|
|
75
77
|
极简 5 项健康打分:engine_up / port_listen / dns_ok / system_proxy_ok /
|
|
76
78
|
connectivity_ok。\fB--json\fR 额外含 informational 字段:
|
|
77
|
-
engine / mode / port / config_path / engine_config_path / lock_held / lock_path
|
|
79
|
+
engine / mode / port / config_path / engine_config_path / lock_held / lock_path /
|
|
80
|
+
\fBhealthy\fR (v0.3.3 新增 bool,agent 不必再算 score==max)。
|
|
78
81
|
.TP
|
|
79
82
|
.B proxyctl help [\fICOMMAND\fR]
|
|
80
83
|
顶层帮助 / 单命令完整说明。等价 \fBproxyctl --help\fR / \fBproxyctl <cmd> --help\fR。
|
|
@@ -1571,6 +1571,8 @@ def cmd_version_print() -> None:
|
|
|
1571
1571
|
"agents_md": True,
|
|
1572
1572
|
"commands_schema": True,
|
|
1573
1573
|
"doctor_extended": True,
|
|
1574
|
+
"doctor_healthy_field": True, # 0.3.3
|
|
1575
|
+
"agent_guide_sections": True, # 0.3.3
|
|
1574
1576
|
"log_ndjson_v2": True,
|
|
1575
1577
|
},
|
|
1576
1578
|
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""proxyctl completion — shell 补全脚本生成。
|
|
2
|
+
|
|
3
|
+
支持 bash / zsh / fish。脚本从 COMMANDS_META + TOPICS 动态派生,
|
|
4
|
+
0.3.x 起补全:
|
|
5
|
+
|
|
6
|
+
- 全局 flag: --help / --version / --json / --no-color / --quiet
|
|
7
|
+
/ --dry-run / --plain
|
|
8
|
+
- explain <topic> → topic 列表
|
|
9
|
+
- mode <tun|proxy> → 模式
|
|
10
|
+
- engine <mihomo|singbox> → 引擎
|
|
11
|
+
- config <path|get|set> → 子命令
|
|
12
|
+
- audit <apply|N> → 子命令 / 常用天数
|
|
13
|
+
- commands [--schema] → 0.3.x schema 子模式
|
|
14
|
+
- agent-guide [--section <name> | --list-sections] → 0.3.3 切片
|
|
15
|
+
- help <cmd> → 顶层命令(与 <cmd> --help 同源)
|
|
16
|
+
- completion <bash|zsh|fish> → shell
|
|
17
|
+
- 写命令位置补 --dry-run;audit/check 位置补 --plain
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
eval "$(proxyctl completion zsh)"
|
|
21
|
+
proxyctl completion bash > ~/.proxyctl.bash && echo 'source ~/.proxyctl.bash' >> ~/.bashrc
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import sys
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# 0.3.x:写命令支持 --dry-run(与 COMMANDS_META.supports_dry_run 同步)
|
|
30
|
+
_DRY_RUN_CMDS = ("mode", "engine", "fix", "audit", "config",
|
|
31
|
+
"daemon", "claude-proxy", "dns-lock", "dns-unlock")
|
|
32
|
+
# 0.3.x:audit / check 支持 --plain
|
|
33
|
+
_PLAIN_CMDS = ("audit", "check")
|
|
34
|
+
# 0.3.x:commands 支持 --schema 子模式
|
|
35
|
+
_SCHEMA_CMDS = ("commands",)
|
|
36
|
+
# 0.3.3:agent-guide 支持 --section / --list-sections
|
|
37
|
+
_AGENT_GUIDE_FLAGS = ("--section", "--list-sections")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _command_names() -> list:
|
|
41
|
+
from proxyctl.explain import COMMANDS_META
|
|
42
|
+
return [c["name"] for c in COMMANDS_META]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _topic_names() -> list:
|
|
46
|
+
from proxyctl.explain import TOPICS
|
|
47
|
+
return sorted(TOPICS.keys())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _gen_bash() -> str:
|
|
51
|
+
cmds = " ".join(_command_names())
|
|
52
|
+
topics = " ".join(_topic_names())
|
|
53
|
+
dry_run_cmds = " ".join(f'"{c}"' for c in _DRY_RUN_CMDS)
|
|
54
|
+
plain_cmds = " ".join(f'"{c}"' for c in _PLAIN_CMDS)
|
|
55
|
+
return f"""# proxyctl bash completion (0.3.x)
|
|
56
|
+
_proxyctl_complete() {{
|
|
57
|
+
local cur prev cmds topics first
|
|
58
|
+
COMPREPLY=()
|
|
59
|
+
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
60
|
+
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
|
61
|
+
cmds="{cmds}"
|
|
62
|
+
topics="{topics}"
|
|
63
|
+
first="${{COMP_WORDS[1]}}"
|
|
64
|
+
|
|
65
|
+
# 第二个 token = 子命令
|
|
66
|
+
if [ ${{COMP_CWORD}} -eq 1 ]; then
|
|
67
|
+
COMPREPLY=( $(compgen -W "${{cmds}} help --help --version --json --plain --dry-run --no-color --quiet" -- "${{cur}}") )
|
|
68
|
+
return 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# help <cmd>(与 <cmd> --help 同源)
|
|
72
|
+
if [ "$first" = "help" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
73
|
+
COMPREPLY=( $(compgen -W "${{cmds}}" -- "${{cur}}") )
|
|
74
|
+
return 0
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# explain <topic>
|
|
78
|
+
if [ "$first" = "explain" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
79
|
+
COMPREPLY=( $(compgen -W "${{topics}}" -- "${{cur}}") )
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# mode <tun|proxy>
|
|
84
|
+
if [ "$first" = "mode" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
85
|
+
COMPREPLY=( $(compgen -W "tun proxy" -- "${{cur}}") )
|
|
86
|
+
return 0
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# engine <mihomo|singbox>
|
|
90
|
+
if [ "$first" = "engine" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
91
|
+
COMPREPLY=( $(compgen -W "mihomo singbox" -- "${{cur}}") )
|
|
92
|
+
return 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# config path|get|set
|
|
96
|
+
if [ "$first" = "config" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
97
|
+
COMPREPLY=( $(compgen -W "path get set" -- "${{cur}}") )
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
# audit <apply|days>
|
|
102
|
+
if [ "$first" = "audit" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
103
|
+
COMPREPLY=( $(compgen -W "apply 1 3 7 30" -- "${{cur}}") )
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# commands --schema 子模式
|
|
108
|
+
if [ "$first" = "commands" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
109
|
+
COMPREPLY=( $(compgen -W "--schema --json" -- "${{cur}}") )
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# agent-guide --section / --list-sections
|
|
114
|
+
if [ "$first" = "agent-guide" ]; then
|
|
115
|
+
if [ "$prev" = "--section" ]; then
|
|
116
|
+
# 这里不知道实际 section list(运行时才有);给个占位
|
|
117
|
+
COMPREPLY=( $(compgen -W "introduction onboarding capabilities envelope locks footgun" -- "${{cur}}") )
|
|
118
|
+
return 0
|
|
119
|
+
fi
|
|
120
|
+
COMPREPLY=( $(compgen -W "--section --list-sections --json" -- "${{cur}}") )
|
|
121
|
+
return 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# completion shell
|
|
125
|
+
if [ "$first" = "completion" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
126
|
+
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${{cur}}") )
|
|
127
|
+
return 0
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# 在写命令的任意位置补 --dry-run
|
|
131
|
+
local dry_run_cmds=({dry_run_cmds})
|
|
132
|
+
for c in "${{dry_run_cmds[@]}}"; do
|
|
133
|
+
if [ "$first" = "$c" ]; then
|
|
134
|
+
COMPREPLY=( $(compgen -W "--dry-run --json --no-color --quiet --help" -- "${{cur}}") )
|
|
135
|
+
return 0
|
|
136
|
+
fi
|
|
137
|
+
done
|
|
138
|
+
|
|
139
|
+
# audit / check 加 --plain
|
|
140
|
+
local plain_cmds=({plain_cmds})
|
|
141
|
+
for c in "${{plain_cmds[@]}}"; do
|
|
142
|
+
if [ "$first" = "$c" ]; then
|
|
143
|
+
COMPREPLY=( $(compgen -W "--plain --json --dry-run --help" -- "${{cur}}") )
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
done
|
|
147
|
+
|
|
148
|
+
# log 特有 flag
|
|
149
|
+
if [ "$first" = "log" ]; then
|
|
150
|
+
COMPREPLY=( $(compgen -W "--tail --no-follow --json --help" -- "${{cur}}") )
|
|
151
|
+
return 0
|
|
152
|
+
fi
|
|
153
|
+
|
|
154
|
+
# env 特有 flag
|
|
155
|
+
if [ "$first" = "env" ]; then
|
|
156
|
+
COMPREPLY=( $(compgen -W "--unset" -- "${{cur}}") )
|
|
157
|
+
return 0
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
# 默认 flag 补全
|
|
161
|
+
COMPREPLY=( $(compgen -W "--help --json --plain --dry-run --no-color --quiet" -- "${{cur}}") )
|
|
162
|
+
}}
|
|
163
|
+
complete -F _proxyctl_complete proxyctl
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _gen_zsh() -> str:
|
|
168
|
+
cmds = "\n".join(f" '{name}:{summary}'" for name, summary in _zsh_pairs())
|
|
169
|
+
topics = " ".join(_topic_names())
|
|
170
|
+
return f"""#compdef proxyctl
|
|
171
|
+
# proxyctl zsh completion (0.3.x)
|
|
172
|
+
|
|
173
|
+
_proxyctl_topics() {{
|
|
174
|
+
local -a topics
|
|
175
|
+
topics=({topics})
|
|
176
|
+
_describe 'topic' topics
|
|
177
|
+
}}
|
|
178
|
+
|
|
179
|
+
_proxyctl_cmd_names() {{
|
|
180
|
+
local -a names
|
|
181
|
+
names=({' '.join(_command_names())})
|
|
182
|
+
_describe 'command' names
|
|
183
|
+
}}
|
|
184
|
+
|
|
185
|
+
_proxyctl() {{
|
|
186
|
+
local context state state_descr line
|
|
187
|
+
typeset -A opt_args
|
|
188
|
+
|
|
189
|
+
local -a cmds
|
|
190
|
+
cmds=(
|
|
191
|
+
{cmds}
|
|
192
|
+
'help:顶层帮助 / 单命令帮助'
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
_arguments -C \\
|
|
196
|
+
'1: :->cmd' \\
|
|
197
|
+
'*: :->args' \\
|
|
198
|
+
'--help[显示帮助]' \\
|
|
199
|
+
'-h[显示帮助]' \\
|
|
200
|
+
'--version[显示版本(加 --json 暴露 supported_features)]' \\
|
|
201
|
+
'-v[显示版本]' \\
|
|
202
|
+
'--json[结构化 envelope v2 输出]' \\
|
|
203
|
+
'--plain[纯 TSV 输出(audit/check);与 --json 互斥]' \\
|
|
204
|
+
'--dry-run[预演写命令;输出 data.plan]' \\
|
|
205
|
+
'--no-color[关闭 ANSI 颜色]' \\
|
|
206
|
+
'--quiet[安静模式]' \\
|
|
207
|
+
'-q[安静模式]'
|
|
208
|
+
|
|
209
|
+
case $state in
|
|
210
|
+
cmd) _describe 'command' cmds ;;
|
|
211
|
+
args)
|
|
212
|
+
case $words[2] in
|
|
213
|
+
help) _proxyctl_cmd_names ;;
|
|
214
|
+
explain) _proxyctl_topics ;;
|
|
215
|
+
mode) _values 'mode' tun proxy ;;
|
|
216
|
+
engine) _values 'engine' mihomo singbox ;;
|
|
217
|
+
config) _values 'subcommand' path get set ;;
|
|
218
|
+
audit) _values 'days_or_apply' apply 1 3 7 30 ;;
|
|
219
|
+
commands) _values 'flag' --schema --json ;;
|
|
220
|
+
completion) _values 'shell' bash zsh fish ;;
|
|
221
|
+
agent-guide)
|
|
222
|
+
if [[ "$words[$CURRENT-1]" == "--section" ]]; then
|
|
223
|
+
# 运行时取真实 section 名(fallback 常见值)
|
|
224
|
+
_values 'section' \\
|
|
225
|
+
introduction onboarding capabilities exclusions \\
|
|
226
|
+
concept-map paths exit-codes envelope decision-tree \\
|
|
227
|
+
non-interactive locks footgun self-discovery repo
|
|
228
|
+
else
|
|
229
|
+
_values 'flag' --section --list-sections --json
|
|
230
|
+
fi
|
|
231
|
+
;;
|
|
232
|
+
log) _values 'flag' --tail --no-follow --json ;;
|
|
233
|
+
env) _values 'flag' --unset ;;
|
|
234
|
+
esac
|
|
235
|
+
;;
|
|
236
|
+
esac
|
|
237
|
+
}}
|
|
238
|
+
|
|
239
|
+
compdef _proxyctl proxyctl
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _zsh_pairs() -> list:
|
|
244
|
+
from proxyctl.explain import COMMANDS_META
|
|
245
|
+
out = []
|
|
246
|
+
for c in COMMANDS_META:
|
|
247
|
+
summary = c["summary"].replace("'", "'\\''")
|
|
248
|
+
out.append((c["name"], summary))
|
|
249
|
+
return out
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _gen_fish() -> str:
|
|
253
|
+
from proxyctl.explain import COMMANDS_META
|
|
254
|
+
lines = ["# proxyctl fish completion (0.3.x)",
|
|
255
|
+
"complete -c proxyctl -l help -d '显示帮助'",
|
|
256
|
+
"complete -c proxyctl -l version -d '显示版本'",
|
|
257
|
+
"complete -c proxyctl -l json -d 'envelope v2 输出'",
|
|
258
|
+
"complete -c proxyctl -l plain -d 'TSV 输出(audit/check)'",
|
|
259
|
+
"complete -c proxyctl -l dry-run -d '预演写命令'",
|
|
260
|
+
"complete -c proxyctl -l no-color -d '关闭 ANSI'",
|
|
261
|
+
"complete -c proxyctl -l quiet -d '安静模式'",
|
|
262
|
+
""]
|
|
263
|
+
# 全局子命令补全(不带子参数时)
|
|
264
|
+
for c in COMMANDS_META:
|
|
265
|
+
summary = c["summary"].replace("'", "")[:60]
|
|
266
|
+
lines.append(
|
|
267
|
+
f"complete -c proxyctl -n '__fish_use_subcommand' "
|
|
268
|
+
f"-a '{c['name']}' -d '{summary}'"
|
|
269
|
+
)
|
|
270
|
+
# help 也作为顶层子命令补
|
|
271
|
+
lines.append(
|
|
272
|
+
"complete -c proxyctl -n '__fish_use_subcommand' "
|
|
273
|
+
"-a 'help' -d '顶层/单命令帮助'"
|
|
274
|
+
)
|
|
275
|
+
# 子参数 / 子 flag
|
|
276
|
+
topics = " ".join(_topic_names())
|
|
277
|
+
lines.append("")
|
|
278
|
+
lines.append(
|
|
279
|
+
f"complete -c proxyctl -n '__fish_seen_subcommand_from explain' "
|
|
280
|
+
f"-a '{topics}'"
|
|
281
|
+
)
|
|
282
|
+
lines.append(
|
|
283
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from help' "
|
|
284
|
+
f"-a '{' '.join(_command_names())}'"
|
|
285
|
+
)
|
|
286
|
+
lines.append(
|
|
287
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from mode' "
|
|
288
|
+
"-a 'tun proxy'"
|
|
289
|
+
)
|
|
290
|
+
lines.append(
|
|
291
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from engine' "
|
|
292
|
+
"-a 'mihomo singbox'"
|
|
293
|
+
)
|
|
294
|
+
lines.append(
|
|
295
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from config' "
|
|
296
|
+
"-a 'path get set'"
|
|
297
|
+
)
|
|
298
|
+
lines.append(
|
|
299
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from completion' "
|
|
300
|
+
"-a 'bash zsh fish'"
|
|
301
|
+
)
|
|
302
|
+
# 0.3.x: 写命令补 --dry-run
|
|
303
|
+
for c in _DRY_RUN_CMDS:
|
|
304
|
+
lines.append(
|
|
305
|
+
f"complete -c proxyctl -n '__fish_seen_subcommand_from {c}' "
|
|
306
|
+
f"-l dry-run -d '预演(不真正执行)'"
|
|
307
|
+
)
|
|
308
|
+
# 0.3.x: audit/check 补 --plain
|
|
309
|
+
for c in _PLAIN_CMDS:
|
|
310
|
+
lines.append(
|
|
311
|
+
f"complete -c proxyctl -n '__fish_seen_subcommand_from {c}' "
|
|
312
|
+
f"-l plain -d 'TSV 输出'"
|
|
313
|
+
)
|
|
314
|
+
# 0.3.x: commands --schema
|
|
315
|
+
lines.append(
|
|
316
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from commands' "
|
|
317
|
+
"-l schema -d '输出 commands JSON Schema'"
|
|
318
|
+
)
|
|
319
|
+
# 0.3.3: agent-guide --section / --list-sections
|
|
320
|
+
lines.append(
|
|
321
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from agent-guide' "
|
|
322
|
+
"-l section -d '只输出指定 section'"
|
|
323
|
+
)
|
|
324
|
+
lines.append(
|
|
325
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from agent-guide' "
|
|
326
|
+
"-l list-sections -d '列出所有 section 名'"
|
|
327
|
+
)
|
|
328
|
+
# log / env 特有 flag
|
|
329
|
+
lines.append(
|
|
330
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from log' "
|
|
331
|
+
"-l tail -d '只取最后 N 行'"
|
|
332
|
+
)
|
|
333
|
+
lines.append(
|
|
334
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from log' "
|
|
335
|
+
"-l no-follow -d '一次性读取后返回'"
|
|
336
|
+
)
|
|
337
|
+
lines.append(
|
|
338
|
+
"complete -c proxyctl -n '__fish_seen_subcommand_from env' "
|
|
339
|
+
"-l unset -d '清除代理环境变量'"
|
|
340
|
+
)
|
|
341
|
+
return "\n".join(lines) + "\n"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
SHELLS = {"bash": _gen_bash, "zsh": _gen_zsh, "fish": _gen_fish}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cmd_completion(args: list) -> None:
|
|
348
|
+
"""proxyctl completion [bash|zsh|fish]"""
|
|
349
|
+
from proxyctl import _io
|
|
350
|
+
try:
|
|
351
|
+
from proxyctl.explain import GLOBAL_FLAGS_REF as _gf
|
|
352
|
+
as_json = bool(_gf().get("json"))
|
|
353
|
+
except Exception:
|
|
354
|
+
as_json = False
|
|
355
|
+
|
|
356
|
+
if not args:
|
|
357
|
+
_io.fail("缺少 shell 参数",
|
|
358
|
+
hint="proxyctl completion [bash|zsh|fish]",
|
|
359
|
+
doc="agent", code=_io.USAGE, cmd="completion",
|
|
360
|
+
as_json=as_json)
|
|
361
|
+
return
|
|
362
|
+
|
|
363
|
+
shell = args[0]
|
|
364
|
+
if shell not in SHELLS:
|
|
365
|
+
import difflib
|
|
366
|
+
suggest = difflib.get_close_matches(shell, list(SHELLS), n=1, cutoff=0.4)
|
|
367
|
+
hints = [f"支持: {', '.join(SHELLS)}"]
|
|
368
|
+
if suggest:
|
|
369
|
+
hints.insert(0, f"是否想要:{suggest[0]}?")
|
|
370
|
+
_io.fail(f"未知 shell:{shell}",
|
|
371
|
+
hints=hints, doc="agent",
|
|
372
|
+
code=_io.USAGE, cmd="completion",
|
|
373
|
+
as_json=as_json)
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
script = SHELLS[shell]()
|
|
377
|
+
if as_json:
|
|
378
|
+
_io.emit_json(_io.envelope("completion",
|
|
379
|
+
data={"shell": shell, "script": script}))
|
|
380
|
+
return
|
|
381
|
+
print(script)
|
|
@@ -493,15 +493,143 @@ def _suggest(name: str, candidates: list) -> list:
|
|
|
493
493
|
# ── 主入口 2: agent-guide ─────────────────────────────────────────────────
|
|
494
494
|
|
|
495
495
|
def cmd_agent_guide(args: list, backend, config) -> None:
|
|
496
|
-
"""proxyctl agent-guide [--
|
|
496
|
+
"""proxyctl agent-guide [--section <name>] [--list-sections] [--json]
|
|
497
|
+
|
|
498
|
+
无参 → 输出完整 markdown(≤300 行)。
|
|
499
|
+
`--list-sections` → 列出所有可用 section 名,agent 可挑一个取。
|
|
500
|
+
`--section <name>` → 只输出该 section 的 markdown(H2 标题下 + 直到下一个 H2)。
|
|
501
|
+
name 是大小写不敏感 + 空格容忍的模糊匹配("envelope fields" / "envelope-fields"
|
|
502
|
+
/ "Envelope 字段含义表" 都接受)。
|
|
503
|
+
"""
|
|
497
504
|
as_json = GLOBAL_FLAGS_REF().get("json", False)
|
|
505
|
+
|
|
506
|
+
# 解析 args
|
|
507
|
+
positional, flags = _io.extract_flags(
|
|
508
|
+
args, known={"--section": "value", "--list-sections": "bool"})
|
|
509
|
+
|
|
498
510
|
text = _build_agent_guide(backend, config)
|
|
511
|
+
sections = _split_agent_guide_sections(text)
|
|
512
|
+
section_names = list(sections.keys()) # 保持原顺序
|
|
513
|
+
|
|
514
|
+
if flags.get("list_sections"):
|
|
515
|
+
if as_json:
|
|
516
|
+
emit_json(envelope("agent-guide", data={
|
|
517
|
+
"available_sections": section_names,
|
|
518
|
+
"section_count": len(section_names),
|
|
519
|
+
}))
|
|
520
|
+
return
|
|
521
|
+
print(f"{BOLD}可用 section({len(section_names)} 个):{NC}")
|
|
522
|
+
for s in section_names:
|
|
523
|
+
print(f" {CYAN}{s}{NC}")
|
|
524
|
+
print(f"\n{DIM}用法:proxyctl agent-guide --section <name>{NC}")
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
requested = flags.get("section")
|
|
528
|
+
if requested:
|
|
529
|
+
match = _match_section(requested, section_names)
|
|
530
|
+
if match is None:
|
|
531
|
+
import difflib
|
|
532
|
+
suggest = difflib.get_close_matches(
|
|
533
|
+
requested, section_names, n=3, cutoff=0.3)
|
|
534
|
+
hints = [f"可用 section: {', '.join(section_names)}"]
|
|
535
|
+
if suggest:
|
|
536
|
+
hints.insert(0, f"是否想要:{suggest[0]}?")
|
|
537
|
+
_io.fail(f"未识别 section:{requested}",
|
|
538
|
+
hints=hints, doc="agent",
|
|
539
|
+
code=_io.USAGE, cmd="agent-guide")
|
|
540
|
+
chunk = sections[match]
|
|
541
|
+
if as_json:
|
|
542
|
+
emit_json(envelope("agent-guide", data={
|
|
543
|
+
"section": match,
|
|
544
|
+
"markdown": chunk,
|
|
545
|
+
"available_sections": section_names,
|
|
546
|
+
}))
|
|
547
|
+
return
|
|
548
|
+
print(chunk)
|
|
549
|
+
return
|
|
550
|
+
|
|
499
551
|
if as_json:
|
|
500
|
-
emit_json(envelope("agent-guide", data={
|
|
552
|
+
emit_json(envelope("agent-guide", data={
|
|
553
|
+
"markdown": text,
|
|
554
|
+
"available_sections": section_names,
|
|
555
|
+
}))
|
|
501
556
|
return
|
|
502
557
|
print(text)
|
|
503
558
|
|
|
504
559
|
|
|
560
|
+
def _split_agent_guide_sections(md: str) -> dict[str, str]:
|
|
561
|
+
"""把 _build_agent_guide 输出的 markdown 按 H2 标题切分。
|
|
562
|
+
|
|
563
|
+
返回 OrderedDict 形式(python 3.7+ 普通 dict 即有序):
|
|
564
|
+
{section_name: section_markdown_including_heading}
|
|
565
|
+
|
|
566
|
+
section_name 是 H2 标题去 emoji / 中文标点后的归一化版(lowercase + 空格连字符化),
|
|
567
|
+
便于 agent 用 ASCII 名稳定引用。
|
|
568
|
+
"""
|
|
569
|
+
import re
|
|
570
|
+
lines = md.split("\n")
|
|
571
|
+
sections: dict[str, str] = {}
|
|
572
|
+
cur_name: str | None = None
|
|
573
|
+
cur_lines: list[str] = []
|
|
574
|
+
# 文档开头到第一个 H2 之前的内容 → "introduction"
|
|
575
|
+
intro_started = False
|
|
576
|
+
for ln in lines:
|
|
577
|
+
m = re.match(r"^##\s+(.+?)\s*$", ln)
|
|
578
|
+
if m:
|
|
579
|
+
# 收尾上一个 section
|
|
580
|
+
if cur_name is not None:
|
|
581
|
+
sections[cur_name] = "\n".join(cur_lines).rstrip() + "\n"
|
|
582
|
+
elif cur_lines:
|
|
583
|
+
# 文档开头部分作为 "introduction"
|
|
584
|
+
sections["introduction"] = "\n".join(cur_lines).rstrip() + "\n"
|
|
585
|
+
title = m.group(1).strip()
|
|
586
|
+
cur_name = _normalize_section_name(title)
|
|
587
|
+
cur_lines = [ln]
|
|
588
|
+
else:
|
|
589
|
+
cur_lines.append(ln)
|
|
590
|
+
if cur_name is not None:
|
|
591
|
+
sections[cur_name] = "\n".join(cur_lines).rstrip() + "\n"
|
|
592
|
+
elif cur_lines and not sections:
|
|
593
|
+
sections["introduction"] = "\n".join(cur_lines).rstrip() + "\n"
|
|
594
|
+
return sections
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _normalize_section_name(title: str) -> str:
|
|
598
|
+
"""H2 标题 → ASCII 友好的 section name(agent 引用稳定)。
|
|
599
|
+
|
|
600
|
+
约定:H2 推荐写成 ``## English Name — 中文标题``,取破折号(—/–/-)前
|
|
601
|
+
的英文段做 ID。这样 agent 引用的是稳定 ASCII slug,人类标题仍然中文友好。
|
|
602
|
+
|
|
603
|
+
映射规则:取破折号前内容(若有)→ 保留 ASCII 字母数字 → 空格合并 →
|
|
604
|
+
转连字符 → lowercase。全空回退 'section'。
|
|
605
|
+
"""
|
|
606
|
+
import re
|
|
607
|
+
# 1. 取破折号前的部分(U+2014 — / U+2013 – / ASCII - 都接受)。
|
|
608
|
+
# 必须两边都有空格才算分隔符,避免误切 "Non-Interactive" / "Self-Discovery"
|
|
609
|
+
# 这类内部含 ASCII 连字符但无空格的英文短语。
|
|
610
|
+
parts = re.split(r"\s+[—–\-]\s+", title, maxsplit=1)
|
|
611
|
+
head = parts[0] if parts else title
|
|
612
|
+
# 2. 留下 ASCII 字母数字、空格、连字符
|
|
613
|
+
s = re.sub(r"[^A-Za-z0-9\s\-]", " ", head)
|
|
614
|
+
s = re.sub(r"\s+", " ", s).strip()
|
|
615
|
+
s = s.replace(" ", "-").lower()
|
|
616
|
+
# 3. 连续连字符合并 + 去边界
|
|
617
|
+
s = re.sub(r"-+", "-", s).strip("-")
|
|
618
|
+
return s or "section"
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _match_section(requested: str, names: list[str]) -> str | None:
|
|
622
|
+
"""模糊匹配:requested 可以是归一化后的 name、原始 H2 标题、或子串。"""
|
|
623
|
+
norm_req = _normalize_section_name(requested)
|
|
624
|
+
if norm_req in names:
|
|
625
|
+
return norm_req
|
|
626
|
+
# 子串匹配(防止 agent 拼 'envelope' 想找 'envelope-fields')
|
|
627
|
+
candidates = [n for n in names if norm_req and norm_req in n]
|
|
628
|
+
if len(candidates) == 1:
|
|
629
|
+
return candidates[0]
|
|
630
|
+
return None
|
|
631
|
+
|
|
632
|
+
|
|
505
633
|
def _build_agent_guide(backend, config) -> str:
|
|
506
634
|
port = config.get("proxy_port", 7890)
|
|
507
635
|
mcfg = backend.config_file
|
|
@@ -523,7 +651,7 @@ def _build_agent_guide(backend, config) -> str:
|
|
|
523
651
|
> 本文档由 `proxyctl agent-guide` 在运行时输出,含当前 backend/路径/端口。
|
|
524
652
|
> 仓库视角(开发/贡献协议)见仓库根 `AGENTS.md`。
|
|
525
653
|
|
|
526
|
-
## Agent 第一次接入:6 步引导路径
|
|
654
|
+
## Onboarding — Agent 第一次接入:6 步引导路径
|
|
527
655
|
|
|
528
656
|
```
|
|
529
657
|
Step 1 proxyctl agent-guide # 你正在看
|
|
@@ -537,7 +665,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
537
665
|
|
|
538
666
|
调用任何写命令前先加 `--dry-run --json` 看 `data.plan`,确认无误再去掉。
|
|
539
667
|
|
|
540
|
-
## 能做什么(按副作用三分类)
|
|
668
|
+
## Capabilities — 能做什么(按副作用三分类)
|
|
541
669
|
|
|
542
670
|
| 类别 | sudo | 命令 |
|
|
543
671
|
|---|---|---|
|
|
@@ -549,7 +677,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
549
677
|
|
|
550
678
|
完整精确表见 `proxyctl commands --json` 的 `side_effects` 与 `conditional_side_effects` 字段。
|
|
551
679
|
|
|
552
|
-
## 不能做什么(去别处改)
|
|
680
|
+
## Exclusions — 不能做什么(去别处改)
|
|
553
681
|
|
|
554
682
|
- 添加 / 修改 / 删除分流规则 → 编辑 `{mcfg}` 的 `rules:` 段
|
|
555
683
|
- 添加节点 / 改订阅 → 编辑 `{mcfg}` 的 `proxies:` / `proxy-providers:` 段
|
|
@@ -558,7 +686,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
558
686
|
- 安装 mihomo / sing-box → `brew install mihomo` 等
|
|
559
687
|
- 重启第三方应用 → 浏览器 / Slack / VSCode 需用户自己重启读 system proxy
|
|
560
688
|
|
|
561
|
-
##
|
|
689
|
+
## Concept Map — "想改 X 去哪"
|
|
562
690
|
|
|
563
691
|
| 想改 | 文件 | 段 / 字段 |
|
|
564
692
|
|---|---|---|
|
|
@@ -571,7 +699,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
571
699
|
更多:`proxyctl explain <topic>`,topic:
|
|
572
700
|
{topics_list}
|
|
573
701
|
|
|
574
|
-
## 关键路径(当前 backend = {backend.name})
|
|
702
|
+
## Paths — 关键路径(当前 backend = {backend.name})
|
|
575
703
|
|
|
576
704
|
- proxyctl 配置: `{pcfg}`
|
|
577
705
|
- 引擎配置: `{mcfg}`
|
|
@@ -581,7 +709,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
581
709
|
- 用户插件目录: `~/.config/proxyctl/plugins/*.py`
|
|
582
710
|
- 锁文件目录: `{lock_dir}/.lock.{{system|config|daemon}}`
|
|
583
711
|
|
|
584
|
-
## 退出码
|
|
712
|
+
## Exit Codes — 退出码
|
|
585
713
|
|
|
586
714
|
```
|
|
587
715
|
{exit_lines}
|
|
@@ -589,7 +717,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
589
717
|
|
|
590
718
|
旧路径仍返回 1(GENERIC)。新错误路径使用分语义码。SIGINT → 130。
|
|
591
719
|
|
|
592
|
-
## JSON envelope(schema v2)
|
|
720
|
+
## Envelope — JSON envelope(schema v2)
|
|
593
721
|
|
|
594
722
|
```json
|
|
595
723
|
{{
|
|
@@ -632,7 +760,7 @@ Step 6 proxyctl explain <topic> # 深入概念(topic 见下)
|
|
|
632
760
|
NDJSON 流式:`bench --json` 每节点一行 JSON + 末尾 envelope summary;
|
|
633
761
|
`log --json` 每行一个 `{{file, line}}` 对象(非 envelope)。
|
|
634
762
|
|
|
635
|
-
## 故障决策树(给 Agent 自动化)
|
|
763
|
+
## Decision Tree — 故障决策树(给 Agent 自动化)
|
|
636
764
|
|
|
637
765
|
1. `proxyctl doctor --json` ← 最快,5 项布尔 + score(+ engine/mode/lock_path 信息字段)
|
|
638
766
|
2. 如果 `engine_up=false` → `proxyctl start`
|
|
@@ -643,13 +771,13 @@ NDJSON 流式:`bench --json` 每节点一行 JSON + 末尾 envelope summary;
|
|
|
643
771
|
7. 切网后 → `proxyctl recover`(不重启进程)
|
|
644
772
|
8. 拿不到锁(exit=8 LOCKED)→ 见下方"锁文件位置 + 手动释放"
|
|
645
773
|
|
|
646
|
-
## non-interactive 承诺
|
|
774
|
+
## Non-Interactive — non-interactive 承诺
|
|
647
775
|
|
|
648
776
|
proxyctl 在 stdin 非 TTY 时**不会**调用 `input()` 等阻塞读取。
|
|
649
777
|
设置 `PROXYCTL_AGENT=1` 等价同时打开 `--json + --no-color + 非交互`,
|
|
650
778
|
所有命令默认输出 envelope v2,写操作摘要打到 stderr。
|
|
651
779
|
|
|
652
|
-
## 锁文件位置 + 手动释放
|
|
780
|
+
## Locks — 锁文件位置 + 手动释放
|
|
653
781
|
|
|
654
782
|
写操作(mode/engine/fix/audit apply/config set/daemon/dns-lock 等)通过
|
|
655
783
|
`fcntl.flock` 保护,并发冲突返回 `LOCKED(8)`。
|
|
@@ -663,7 +791,7 @@ proxyctl 在 stdin 非 TTY 时**不会**调用 `input()` 等阻塞读取。
|
|
|
663
791
|
|
|
664
792
|
LOCKED 错误的 `hints` 列表已包含具体锁路径。
|
|
665
793
|
|
|
666
|
-
## footgun
|
|
794
|
+
## Footgun — footgun 提醒
|
|
667
795
|
|
|
668
796
|
- `mode tun` 需要 sudo;macOS launchd 已自动 sudo prompt
|
|
669
797
|
- `audit apply` 会写引擎 `rules:` 段;建议先 `proxyctl audit apply --dry-run`
|
|
@@ -672,7 +800,7 @@ LOCKED 错误的 `hints` 列表已包含具体锁路径。
|
|
|
672
800
|
- 节点订阅由 mihomo `proxy-providers` 自管;proxyctl 不会刷新订阅
|
|
673
801
|
- `--quiet` 仅压制非关键 stderr;envelope / error 仍输出
|
|
674
802
|
|
|
675
|
-
## 自发现
|
|
803
|
+
## Self-Discovery — 自发现
|
|
676
804
|
|
|
677
805
|
- `proxyctl --version --json` — schema_version + supported_features 探测
|
|
678
806
|
- `proxyctl commands --json` — 全部命令元数据
|
|
@@ -681,7 +809,7 @@ LOCKED 错误的 `hints` 列表已包含具体锁路径。
|
|
|
681
809
|
- `proxyctl help <cmd>` — 单命令完整说明
|
|
682
810
|
- `proxyctl doctor --json` — 健康基线
|
|
683
811
|
|
|
684
|
-
## 仓库视角
|
|
812
|
+
## Repo — 仓库视角
|
|
685
813
|
|
|
686
814
|
如果你正在编辑 proxyctl 源码(而非调用安装好的 CLI),仓库根的
|
|
687
815
|
`AGENTS.md` 含开发约定(DISPATCH 注册 / 错误路径 / 锁 / 提交规范)。
|
|
@@ -855,10 +983,14 @@ COMMANDS_META: list[dict] = [
|
|
|
855
983
|
"needs_sudo": False, "interactive": False, "exit_codes": [0, 2],
|
|
856
984
|
"examples": ["proxyctl explain", "proxyctl explain rules --json"]},
|
|
857
985
|
{"name": "agent-guide", "group": "agent",
|
|
858
|
-
"summary": "给 LLM Agent 的入门 markdown
|
|
986
|
+
"summary": "给 LLM Agent 的入门 markdown(含能力边界、退出码、决策树);"
|
|
987
|
+
"支持 --section <name> 按需取小块",
|
|
859
988
|
"args": [], "supports_json": True, "side_effects": [],
|
|
860
|
-
"needs_sudo": False, "interactive": False, "exit_codes": [0],
|
|
861
|
-
"examples": ["proxyctl agent-guide",
|
|
989
|
+
"needs_sudo": False, "interactive": False, "exit_codes": [0, 2],
|
|
990
|
+
"examples": ["proxyctl agent-guide",
|
|
991
|
+
"proxyctl agent-guide --list-sections",
|
|
992
|
+
"proxyctl agent-guide --section envelope --json",
|
|
993
|
+
"proxyctl agent-guide --json"]},
|
|
862
994
|
{"name": "commands", "group": "agent",
|
|
863
995
|
"summary": "所有命令的元数据(含 side_effects / needs_sudo / exit_codes);"
|
|
864
996
|
"--schema 输出 JSON Schema",
|
|
@@ -1233,9 +1365,12 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1233
1365
|
held = []
|
|
1234
1366
|
lock_path_map = _io.lock_paths()
|
|
1235
1367
|
|
|
1368
|
+
healthy = (score == len(flags))
|
|
1236
1369
|
data = {
|
|
1237
1370
|
**flags,
|
|
1238
|
-
"score": score, "max": len(flags),
|
|
1371
|
+
"score": score, "max": len(flags),
|
|
1372
|
+
"healthy": healthy, # 0.3.3:agent 不必自己算 score == max
|
|
1373
|
+
"hint": hint,
|
|
1239
1374
|
# informational fields (W15 in 0.3.0):
|
|
1240
1375
|
"engine": backend.name,
|
|
1241
1376
|
"mode": mode_str,
|
|
@@ -1245,7 +1380,6 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1245
1380
|
"lock_held": held,
|
|
1246
1381
|
"lock_path": lock_path_map,
|
|
1247
1382
|
}
|
|
1248
|
-
healthy = (score == len(flags))
|
|
1249
1383
|
code = OK if healthy else ENGINE_DOWN
|
|
1250
1384
|
|
|
1251
1385
|
if as_json:
|
|
@@ -1342,5 +1476,12 @@ def GLOBAL_FLAGS_REF() -> dict:
|
|
|
1342
1476
|
|
|
1343
1477
|
|
|
1344
1478
|
def set_global_flags(flags: dict) -> None:
|
|
1479
|
+
"""让 explain 模块 + _io 模块的 json 模式保持同步。
|
|
1480
|
+
|
|
1481
|
+
cli.main() 调一次即可;测试若想绕过 cli.main 直接调子命令,也应该用
|
|
1482
|
+
这个 setter(而不是手工写 _GLOBAL_FLAGS),否则 _io.fail / _io.is_json_mode
|
|
1483
|
+
会读到旧值,错误路径不输出 envelope。
|
|
1484
|
+
"""
|
|
1345
1485
|
_GLOBAL_FLAGS.clear()
|
|
1346
1486
|
_GLOBAL_FLAGS.update(flags)
|
|
1487
|
+
_io.set_json_mode(bool(flags.get("json", False)))
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
"""proxyctl completion — shell 补全脚本生成。
|
|
2
|
-
|
|
3
|
-
支持 bash / zsh / fish。脚本从 COMMANDS_META + TOPICS 动态派生。
|
|
4
|
-
|
|
5
|
-
用法:
|
|
6
|
-
eval "$(proxyctl completion zsh)" # 立即生效
|
|
7
|
-
proxyctl completion bash > ~/.proxyctl.bash && echo 'source ~/.proxyctl.bash' >> ~/.bashrc
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import sys
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _command_names() -> list:
|
|
16
|
-
from proxyctl.explain import COMMANDS_META
|
|
17
|
-
return [c["name"] for c in COMMANDS_META]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def _topic_names() -> list:
|
|
21
|
-
from proxyctl.explain import TOPICS
|
|
22
|
-
return sorted(TOPICS.keys())
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _gen_bash() -> str:
|
|
26
|
-
cmds = " ".join(_command_names())
|
|
27
|
-
topics = " ".join(_topic_names())
|
|
28
|
-
return f"""# proxyctl bash completion
|
|
29
|
-
_proxyctl_complete() {{
|
|
30
|
-
local cur prev cmds topics
|
|
31
|
-
COMPREPLY=()
|
|
32
|
-
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
33
|
-
prev="${{COMP_WORDS[COMP_CWORD-1]}}"
|
|
34
|
-
cmds="{cmds}"
|
|
35
|
-
topics="{topics}"
|
|
36
|
-
|
|
37
|
-
# 第二个 token = 子命令
|
|
38
|
-
if [ ${{COMP_CWORD}} -eq 1 ]; then
|
|
39
|
-
COMPREPLY=( $(compgen -W "${{cmds}} --help --version --json --no-color --quiet" -- "${{cur}}") )
|
|
40
|
-
return 0
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
# explain <topic>
|
|
44
|
-
if [ "${{COMP_WORDS[1]}}" = "explain" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
45
|
-
COMPREPLY=( $(compgen -W "${{topics}}" -- "${{cur}}") )
|
|
46
|
-
return 0
|
|
47
|
-
fi
|
|
48
|
-
|
|
49
|
-
# mode <tun|proxy>
|
|
50
|
-
if [ "${{COMP_WORDS[1]}}" = "mode" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
51
|
-
COMPREPLY=( $(compgen -W "tun proxy" -- "${{cur}}") )
|
|
52
|
-
return 0
|
|
53
|
-
fi
|
|
54
|
-
|
|
55
|
-
# engine <mihomo|singbox>
|
|
56
|
-
if [ "${{COMP_WORDS[1]}}" = "engine" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
57
|
-
COMPREPLY=( $(compgen -W "mihomo singbox" -- "${{cur}}") )
|
|
58
|
-
return 0
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
# config path|get|set
|
|
62
|
-
if [ "${{COMP_WORDS[1]}}" = "config" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
63
|
-
COMPREPLY=( $(compgen -W "path get set" -- "${{cur}}") )
|
|
64
|
-
return 0
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# audit apply 子参数
|
|
68
|
-
if [ "${{COMP_WORDS[1]}}" = "audit" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
69
|
-
COMPREPLY=( $(compgen -W "apply 1 3 7 30" -- "${{cur}}") )
|
|
70
|
-
return 0
|
|
71
|
-
fi
|
|
72
|
-
|
|
73
|
-
# completion shell
|
|
74
|
-
if [ "${{COMP_WORDS[1]}}" = "completion" ] && [ ${{COMP_CWORD}} -eq 2 ]; then
|
|
75
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "${{cur}}") )
|
|
76
|
-
return 0
|
|
77
|
-
fi
|
|
78
|
-
|
|
79
|
-
# 默认 flag 补全
|
|
80
|
-
COMPREPLY=( $(compgen -W "--help --json --no-color --quiet" -- "${{cur}}") )
|
|
81
|
-
}}
|
|
82
|
-
complete -F _proxyctl_complete proxyctl
|
|
83
|
-
"""
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _gen_zsh() -> str:
|
|
87
|
-
cmds = "\n".join(f" '{name}:{summary}'" for name, summary in _zsh_pairs())
|
|
88
|
-
topics = " ".join(_topic_names())
|
|
89
|
-
return f"""#compdef proxyctl
|
|
90
|
-
# proxyctl zsh completion
|
|
91
|
-
|
|
92
|
-
_proxyctl_topics() {{
|
|
93
|
-
local -a topics
|
|
94
|
-
topics=({topics})
|
|
95
|
-
_describe 'topic' topics
|
|
96
|
-
}}
|
|
97
|
-
|
|
98
|
-
_proxyctl() {{
|
|
99
|
-
local context state state_descr line
|
|
100
|
-
typeset -A opt_args
|
|
101
|
-
|
|
102
|
-
local -a cmds
|
|
103
|
-
cmds=(
|
|
104
|
-
{cmds}
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
_arguments -C \\
|
|
108
|
-
'1: :->cmd' \\
|
|
109
|
-
'*: :->args' \\
|
|
110
|
-
'--help[显示帮助]' \\
|
|
111
|
-
'--version[显示版本]' \\
|
|
112
|
-
'--json[结构化 JSON 输出]' \\
|
|
113
|
-
'--no-color[关闭 ANSI 颜色]' \\
|
|
114
|
-
'--quiet[安静模式]'
|
|
115
|
-
|
|
116
|
-
case $state in
|
|
117
|
-
cmd) _describe 'command' cmds ;;
|
|
118
|
-
args)
|
|
119
|
-
case $words[2] in
|
|
120
|
-
explain) _proxyctl_topics ;;
|
|
121
|
-
mode) _values 'mode' tun proxy ;;
|
|
122
|
-
engine) _values 'engine' mihomo singbox ;;
|
|
123
|
-
config) _values 'subcommand' path get set ;;
|
|
124
|
-
audit) _values 'days_or_apply' apply 1 3 7 30 ;;
|
|
125
|
-
completion) _values 'shell' bash zsh fish ;;
|
|
126
|
-
esac
|
|
127
|
-
;;
|
|
128
|
-
esac
|
|
129
|
-
}}
|
|
130
|
-
|
|
131
|
-
compdef _proxyctl proxyctl
|
|
132
|
-
"""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _zsh_pairs() -> list:
|
|
136
|
-
from proxyctl.explain import COMMANDS_META
|
|
137
|
-
out = []
|
|
138
|
-
for c in COMMANDS_META:
|
|
139
|
-
summary = c["summary"].replace("'", "'\\''")
|
|
140
|
-
out.append((c["name"], summary))
|
|
141
|
-
return out
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def _gen_fish() -> str:
|
|
145
|
-
from proxyctl.explain import COMMANDS_META
|
|
146
|
-
lines = ["# proxyctl fish completion",
|
|
147
|
-
"complete -c proxyctl -l help -d '显示帮助'",
|
|
148
|
-
"complete -c proxyctl -l version -d '显示版本'",
|
|
149
|
-
"complete -c proxyctl -l json -d '结构化 JSON 输出'",
|
|
150
|
-
"complete -c proxyctl -l no-color -d '关闭 ANSI'",
|
|
151
|
-
"complete -c proxyctl -l quiet -d '安静模式'",
|
|
152
|
-
""]
|
|
153
|
-
# 全局子命令补全(不带子参数时)
|
|
154
|
-
for c in COMMANDS_META:
|
|
155
|
-
summary = c["summary"].replace("'", "")[:60]
|
|
156
|
-
lines.append(
|
|
157
|
-
f"complete -c proxyctl -n '__fish_use_subcommand' "
|
|
158
|
-
f"-a '{c['name']}' -d '{summary}'"
|
|
159
|
-
)
|
|
160
|
-
# explain topic
|
|
161
|
-
topics = " ".join(_topic_names())
|
|
162
|
-
lines.append("")
|
|
163
|
-
lines.append(
|
|
164
|
-
f"complete -c proxyctl -n '__fish_seen_subcommand_from explain' "
|
|
165
|
-
f"-a '{topics}'"
|
|
166
|
-
)
|
|
167
|
-
lines.append(
|
|
168
|
-
"complete -c proxyctl -n '__fish_seen_subcommand_from mode' "
|
|
169
|
-
"-a 'tun proxy'"
|
|
170
|
-
)
|
|
171
|
-
lines.append(
|
|
172
|
-
"complete -c proxyctl -n '__fish_seen_subcommand_from engine' "
|
|
173
|
-
"-a 'mihomo singbox'"
|
|
174
|
-
)
|
|
175
|
-
lines.append(
|
|
176
|
-
"complete -c proxyctl -n '__fish_seen_subcommand_from config' "
|
|
177
|
-
"-a 'path get set'"
|
|
178
|
-
)
|
|
179
|
-
lines.append(
|
|
180
|
-
"complete -c proxyctl -n '__fish_seen_subcommand_from completion' "
|
|
181
|
-
"-a 'bash zsh fish'"
|
|
182
|
-
)
|
|
183
|
-
return "\n".join(lines) + "\n"
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
SHELLS = {"bash": _gen_bash, "zsh": _gen_zsh, "fish": _gen_fish}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def cmd_completion(args: list) -> None:
|
|
190
|
-
"""proxyctl completion [bash|zsh|fish]"""
|
|
191
|
-
from proxyctl import _io
|
|
192
|
-
try:
|
|
193
|
-
from proxyctl.explain import GLOBAL_FLAGS_REF as _gf
|
|
194
|
-
as_json = bool(_gf().get("json"))
|
|
195
|
-
except Exception:
|
|
196
|
-
as_json = False
|
|
197
|
-
|
|
198
|
-
if not args:
|
|
199
|
-
_io.fail("缺少 shell 参数",
|
|
200
|
-
hint="proxyctl completion [bash|zsh|fish]",
|
|
201
|
-
doc="agent", code=_io.USAGE, cmd="completion",
|
|
202
|
-
as_json=as_json)
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
shell = args[0]
|
|
206
|
-
if shell not in SHELLS:
|
|
207
|
-
import difflib
|
|
208
|
-
suggest = difflib.get_close_matches(shell, list(SHELLS), n=1, cutoff=0.4)
|
|
209
|
-
hints = [f"支持: {', '.join(SHELLS)}"]
|
|
210
|
-
if suggest:
|
|
211
|
-
hints.insert(0, f"是否想要:{suggest[0]}?")
|
|
212
|
-
_io.fail(f"未知 shell:{shell}",
|
|
213
|
-
hints=hints, doc="agent",
|
|
214
|
-
code=_io.USAGE, cmd="completion",
|
|
215
|
-
as_json=as_json)
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
script = SHELLS[shell]()
|
|
219
|
-
if as_json:
|
|
220
|
-
_io.emit_json(_io.envelope("completion",
|
|
221
|
-
data={"shell": shell, "script": script}))
|
|
222
|
-
return
|
|
223
|
-
print(script)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|