multi-claude-cli 0.4.0__py3-none-any.whl
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.
- multi_claude/__init__.py +3 -0
- multi_claude/__main__.py +12 -0
- multi_claude/accounts.py +434 -0
- multi_claude/actions.py +66 -0
- multi_claude/cli.py +1302 -0
- multi_claude/completion.py +281 -0
- multi_claude/config.py +488 -0
- multi_claude/doctor.py +221 -0
- multi_claude/env.py +46 -0
- multi_claude/fsutil.py +234 -0
- multi_claude/hook.py +90 -0
- multi_claude/identity.py +94 -0
- multi_claude/launcher.py +193 -0
- multi_claude/lock.py +47 -0
- multi_claude/migrate.py +709 -0
- multi_claude/platform.py +548 -0
- multi_claude/routes.py +133 -0
- multi_claude/sessions.py +195 -0
- multi_claude/shared.py +130 -0
- multi_claude/shellpath.py +33 -0
- multi_claude/statusline.py +196 -0
- multi_claude/usage.py +243 -0
- multi_claude_cli-0.4.0.dist-info/METADATA +457 -0
- multi_claude_cli-0.4.0.dist-info/RECORD +28 -0
- multi_claude_cli-0.4.0.dist-info/WHEEL +5 -0
- multi_claude_cli-0.4.0.dist-info/entry_points.txt +2 -0
- multi_claude_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
- multi_claude_cli-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""`multi-claude completion SHELL`:生成 bash / zsh / fish 补全脚本(方案 v0.2 §5.1.5)。
|
|
2
|
+
|
|
3
|
+
子命令与选项从 argparse 解析器遍历得到,避免手写清单与实际参数漂移。遍历用到
|
|
4
|
+
`parser._actions` 与 `argparse._SubParsersAction.choices` 两个未写入文档的属性,
|
|
5
|
+
兼容性由测试在 Python 3.8 与 3.12 上兜底。账号名在补全时调用 `multi-claude list --names` 获取。
|
|
6
|
+
本模块只输出脚本文本,不修改任何 shell 配置文件。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import shlex
|
|
11
|
+
from typing import Dict, List, NamedTuple
|
|
12
|
+
|
|
13
|
+
SHELLS = ("bash", "zsh", "fish")
|
|
14
|
+
|
|
15
|
+
# 各子命令第 N 个位置参数的候选类型:name 账号名、dir 目录、file 文件、shell 补全的 shell 名、
|
|
16
|
+
# proxy 代理固定值、statusline 该子命令的动作。
|
|
17
|
+
POSITIONAL_KINDS = {
|
|
18
|
+
"migrate-default": ["name"], "add": ["name"], "proxy": ["name", "proxy"], "env": ["name"],
|
|
19
|
+
"args": ["name"], "remove": ["name"], "usage": ["name"], "completion": ["shell"],
|
|
20
|
+
"route": ["dir", "name"], "which": ["dir"], "rename": ["name"], "mcp": ["name"],
|
|
21
|
+
"statusline": ["statusline", "file"], "handoff": ["name"], "login": ["name"], "set": ["name"],
|
|
22
|
+
}
|
|
23
|
+
# 选项值是路径、应按文件补全的选项。
|
|
24
|
+
PATH_OPTIONS = ("-f", "--file", "--root", "--bin-dir", "--shared-dir")
|
|
25
|
+
# 选项值是账号名的选项。
|
|
26
|
+
NAME_OPTIONS = ("--default", "--from")
|
|
27
|
+
FIXED_WORDS = {"shell": SHELLS, "proxy": ("off", "inherit"), "statusline": ("install", "uninstall")}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _positional_cases(indent: str) -> str:
|
|
31
|
+
"""生成 `case "$cmd:$npos" in` 的分支,按候选类型合并。"""
|
|
32
|
+
by_kind: Dict[str, List[str]] = {}
|
|
33
|
+
for command, kinds in sorted(POSITIONAL_KINDS.items()):
|
|
34
|
+
for index, kind in enumerate(kinds):
|
|
35
|
+
by_kind.setdefault(kind, []).append("{}:{}".format(command, index))
|
|
36
|
+
return "\n".join("{}{}) kind={} ;;".format(indent, "|".join(keys), kind) for kind, keys in sorted(by_kind.items()))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CommandSpec(NamedTuple):
|
|
40
|
+
options: List[str]
|
|
41
|
+
# 需要跟一个值的选项(补全时要跳过它的值,才能数对位置参数)
|
|
42
|
+
value_options: List[str]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def collect(parser: argparse.ArgumentParser) -> Dict[str, CommandSpec]:
|
|
46
|
+
"""返回 {子命令: CommandSpec};键 "" 表示顶层(尚未输入子命令时)。"""
|
|
47
|
+
specs = {"": CommandSpec(_options(parser), [])}
|
|
48
|
+
for action in parser._actions:
|
|
49
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
50
|
+
for name, sub in action.choices.items():
|
|
51
|
+
specs[name] = CommandSpec(_options(sub), _value_options(sub))
|
|
52
|
+
return specs
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _options(parser: argparse.ArgumentParser) -> List[str]:
|
|
56
|
+
return [option for action in parser._actions for option in action.option_strings]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _value_options(parser: argparse.ArgumentParser) -> List[str]:
|
|
60
|
+
# nargs == 0 的是开关(store_true / help / version 等),其余选项都要跟值。
|
|
61
|
+
return [option for action in parser._actions if action.option_strings and action.nargs != 0
|
|
62
|
+
for option in action.option_strings]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def render(shell: str, parser: argparse.ArgumentParser) -> str:
|
|
66
|
+
specs = collect(parser)
|
|
67
|
+
if shell == "bash":
|
|
68
|
+
return _bash(specs)
|
|
69
|
+
if shell == "zsh":
|
|
70
|
+
return _zsh(specs)
|
|
71
|
+
return _fish(specs)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _words(items: List[str]) -> str:
|
|
75
|
+
return " ".join(items)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _bash(specs: Dict[str, CommandSpec]) -> str:
|
|
79
|
+
commands = sorted(name for name in specs if name)
|
|
80
|
+
option_cases = "\n".join(" {}) opts={}; vopts={} ;;".format(
|
|
81
|
+
name, shlex.quote(_words(spec.options)), shlex.quote(_words(spec.value_options)))
|
|
82
|
+
for name, spec in sorted(specs.items()) if name)
|
|
83
|
+
return """# bash completion for multi-claude (generated by `multi-claude completion bash`)
|
|
84
|
+
_multi_claude() {
|
|
85
|
+
# COMP_WORDBREAKS 默认含 @ 与 :,COMP_WORDS 会把账号名 a@b 拆开,所以自己从 COMP_LINE 取完整的当前词。
|
|
86
|
+
local line="${COMP_LINE:0:COMP_POINT}"
|
|
87
|
+
local cur="${line##* }"
|
|
88
|
+
local before="${line%%"$cur"}"
|
|
89
|
+
local -a words
|
|
90
|
+
read -r -a words <<< "$before"
|
|
91
|
+
# 第一个不以 - 开头的词是子命令。
|
|
92
|
+
local cmd="" i w
|
|
93
|
+
for ((i = 1; i < ${#words[@]}; i++)); do
|
|
94
|
+
if [[ "${words[i]}" != -* ]]; then cmd="${words[i]}"; break; fi
|
|
95
|
+
done
|
|
96
|
+
local opts="" vopts=""
|
|
97
|
+
case "$cmd" in
|
|
98
|
+
%(option_cases)s
|
|
99
|
+
esac
|
|
100
|
+
# 数子命令之后已输入的位置参数个数,跳过选项与它的值;遇到 -- 记为 -1(args 的参数列表不补全)。
|
|
101
|
+
local npos=0 skip=0 prev=""
|
|
102
|
+
for ((i = i + 1; i < ${#words[@]}; i++)); do
|
|
103
|
+
w="${words[i]}"
|
|
104
|
+
if ((skip)); then skip=0; prev=""; continue; fi
|
|
105
|
+
if [[ "$w" == "--" ]]; then npos=-1; break; fi
|
|
106
|
+
if [[ "$w" == -* ]]; then
|
|
107
|
+
if [[ " $vopts " == *" $w "* ]]; then skip=1; prev="$w"; fi
|
|
108
|
+
continue
|
|
109
|
+
fi
|
|
110
|
+
((npos++))
|
|
111
|
+
done
|
|
112
|
+
local candidates="" kind=""
|
|
113
|
+
if ((skip)); then
|
|
114
|
+
case "$prev" in
|
|
115
|
+
%(path_options)s) COMPREPLY=($(compgen -f -- "$cur")); return 0 ;;
|
|
116
|
+
%(name_options)s) kind=name ;;
|
|
117
|
+
*) COMPREPLY=(); return 0 ;;
|
|
118
|
+
esac
|
|
119
|
+
elif [[ -z "$cmd" ]]; then
|
|
120
|
+
candidates="%(commands)s --help --version"
|
|
121
|
+
elif ((npos < 0)); then
|
|
122
|
+
COMPREPLY=(); return 0
|
|
123
|
+
elif [[ "$cur" == -* ]]; then
|
|
124
|
+
candidates="$opts"
|
|
125
|
+
else
|
|
126
|
+
case "$cmd:$npos" in
|
|
127
|
+
%(positional_cases)s
|
|
128
|
+
esac
|
|
129
|
+
fi
|
|
130
|
+
case "$kind" in
|
|
131
|
+
name) candidates="$(multi-claude list --names 2>/dev/null)" ;;
|
|
132
|
+
dir) COMPREPLY=($(compgen -d -- "$cur")); return 0 ;;
|
|
133
|
+
file) COMPREPLY=($(compgen -f -- "$cur")); return 0 ;;
|
|
134
|
+
shell) candidates="%(shells)s" ;;
|
|
135
|
+
proxy) candidates="off inherit" ;;
|
|
136
|
+
statusline) candidates="install uninstall" ;;
|
|
137
|
+
esac
|
|
138
|
+
COMPREPLY=($(compgen -W "$candidates" -- "$cur"))
|
|
139
|
+
# bash 只替换当前词最后一个分词字符之后的片段,候选要去掉同样长的前缀。
|
|
140
|
+
local j prefix=""
|
|
141
|
+
for ((j = ${#cur} - 1; j >= 0; j--)); do
|
|
142
|
+
if [[ "$COMP_WORDBREAKS" == *"${cur:j:1}"* ]]; then prefix="${cur:0:j+1}"; break; fi
|
|
143
|
+
done
|
|
144
|
+
if [[ -n "$prefix" ]]; then
|
|
145
|
+
for i in "${!COMPREPLY[@]}"; do COMPREPLY[i]="${COMPREPLY[i]#"$prefix"}"; done
|
|
146
|
+
fi
|
|
147
|
+
}
|
|
148
|
+
complete -F _multi_claude multi-claude
|
|
149
|
+
""" % {"option_cases": option_cases, "commands": _words(commands), "shells": _words(list(SHELLS)),
|
|
150
|
+
"positional_cases": _positional_cases(" "), "path_options": "|".join(PATH_OPTIONS),
|
|
151
|
+
"name_options": "|".join(NAME_OPTIONS)}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _zsh(specs: Dict[str, CommandSpec]) -> str:
|
|
155
|
+
commands = sorted(name for name in specs if name)
|
|
156
|
+
option_cases = "\n".join(" {}) opts=({}); vopts=({}) ;;".format(
|
|
157
|
+
name, _words([shlex.quote(o) for o in spec.options]),
|
|
158
|
+
_words([shlex.quote(o) for o in spec.value_options]))
|
|
159
|
+
for name, spec in sorted(specs.items()) if name)
|
|
160
|
+
return """#compdef multi-claude
|
|
161
|
+
# zsh completion for multi-claude (generated by `multi-claude completion zsh`)
|
|
162
|
+
_multi_claude() {
|
|
163
|
+
local cmd="" i w
|
|
164
|
+
local -a opts vopts candidates
|
|
165
|
+
# zsh 的 $words 不按 @ 分词,可直接使用;第一个不以 - 开头的词是子命令。
|
|
166
|
+
for ((i = 2; i < CURRENT; i++)); do
|
|
167
|
+
if [[ "${words[i]}" != -* ]]; then cmd="${words[i]}"; break; fi
|
|
168
|
+
done
|
|
169
|
+
case "$cmd" in
|
|
170
|
+
%(option_cases)s
|
|
171
|
+
esac
|
|
172
|
+
local npos=0 skip=0 prev=""
|
|
173
|
+
for ((i = i + 1; i < CURRENT; i++)); do
|
|
174
|
+
w="${words[i]}"
|
|
175
|
+
if ((skip)); then skip=0; prev=""; continue; fi
|
|
176
|
+
if [[ "$w" == "--" ]]; then npos=-1; break; fi
|
|
177
|
+
if [[ "$w" == -* ]]; then
|
|
178
|
+
if (( ${vopts[(Ie)$w]} )); then skip=1; prev="$w"; fi
|
|
179
|
+
continue
|
|
180
|
+
fi
|
|
181
|
+
((npos++))
|
|
182
|
+
done
|
|
183
|
+
local cur="${words[CURRENT]}" kind=""
|
|
184
|
+
if ((skip)); then
|
|
185
|
+
case "$prev" in
|
|
186
|
+
%(path_options)s) _files; return ;;
|
|
187
|
+
%(name_options)s) kind=name ;;
|
|
188
|
+
*) return ;;
|
|
189
|
+
esac
|
|
190
|
+
elif [[ -z "$cmd" ]]; then
|
|
191
|
+
candidates=(%(commands)s --help --version)
|
|
192
|
+
elif ((npos < 0)); then
|
|
193
|
+
return
|
|
194
|
+
elif [[ "$cur" == -* ]]; then
|
|
195
|
+
candidates=($opts)
|
|
196
|
+
else
|
|
197
|
+
case "$cmd:$npos" in
|
|
198
|
+
%(positional_cases)s
|
|
199
|
+
esac
|
|
200
|
+
fi
|
|
201
|
+
case "$kind" in
|
|
202
|
+
name) candidates=(${(f)"$(multi-claude list --names 2>/dev/null)"}) ;;
|
|
203
|
+
dir) _files -/; return ;;
|
|
204
|
+
file) _files; return ;;
|
|
205
|
+
shell) candidates=(%(shells)s) ;;
|
|
206
|
+
proxy) candidates=(off inherit) ;;
|
|
207
|
+
statusline) candidates=(install uninstall) ;;
|
|
208
|
+
esac
|
|
209
|
+
compadd -- $candidates
|
|
210
|
+
}
|
|
211
|
+
compdef _multi_claude multi-claude
|
|
212
|
+
""" % {"option_cases": option_cases, "commands": _words(commands), "shells": _words(list(SHELLS)),
|
|
213
|
+
"positional_cases": _positional_cases(" "), "path_options": "|".join(PATH_OPTIONS),
|
|
214
|
+
"name_options": "|".join(NAME_OPTIONS)}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _fish(specs: Dict[str, CommandSpec]) -> str:
|
|
218
|
+
commands = sorted(name for name in specs if name)
|
|
219
|
+
lines = [
|
|
220
|
+
"# fish completion for multi-claude (generated by `multi-claude completion fish`)",
|
|
221
|
+
"complete -c multi-claude -f",
|
|
222
|
+
"function __multi_claude_positionals",
|
|
223
|
+
" # 输出当前子命令之后已输入的位置参数个数(跳过选项与它的值);遇到 -- 输出 -1。",
|
|
224
|
+
" set -l tokens (commandline -opc)",
|
|
225
|
+
" set -l value_options $argv",
|
|
226
|
+
" set -l seen 0",
|
|
227
|
+
" set -l skip 0",
|
|
228
|
+
" set -l count 0",
|
|
229
|
+
" for token in $tokens[2..-1]",
|
|
230
|
+
" if test $seen -eq 0",
|
|
231
|
+
" if not string match -q -- '-*' $token",
|
|
232
|
+
" set seen 1",
|
|
233
|
+
" end",
|
|
234
|
+
" continue",
|
|
235
|
+
" end",
|
|
236
|
+
" if test $skip -eq 1",
|
|
237
|
+
" set skip 0",
|
|
238
|
+
" continue",
|
|
239
|
+
" end",
|
|
240
|
+
" if test \"$token\" = '--'",
|
|
241
|
+
" echo -1",
|
|
242
|
+
" return",
|
|
243
|
+
" end",
|
|
244
|
+
" if string match -q -- '-*' $token",
|
|
245
|
+
" if contains -- $token $value_options",
|
|
246
|
+
" set skip 1",
|
|
247
|
+
" end",
|
|
248
|
+
" continue",
|
|
249
|
+
" end",
|
|
250
|
+
" set count (math $count + 1)",
|
|
251
|
+
" end",
|
|
252
|
+
" echo $count",
|
|
253
|
+
"end",
|
|
254
|
+
"complete -c multi-claude -n __fish_use_subcommand -a {}".format(shlex.quote(_words(commands))),
|
|
255
|
+
"complete -c multi-claude -n __fish_use_subcommand -l help -l version",
|
|
256
|
+
]
|
|
257
|
+
for name, spec in sorted(specs.items()):
|
|
258
|
+
if not name:
|
|
259
|
+
continue
|
|
260
|
+
condition = "__fish_seen_subcommand_from {}".format(name)
|
|
261
|
+
for option in spec.options:
|
|
262
|
+
flag = "-l {}".format(option[2:]) if option.startswith("--") else "-s {}".format(option[1:])
|
|
263
|
+
requires = " -r" if option in spec.value_options else ""
|
|
264
|
+
files = " -F" if option in PATH_OPTIONS else ""
|
|
265
|
+
names = " -a '(multi-claude list --names 2>/dev/null)'" if option in NAME_OPTIONS else ""
|
|
266
|
+
lines.append("complete -c multi-claude -n {} {}{}{}{}".format(
|
|
267
|
+
shlex.quote(condition), flag, requires, files, names))
|
|
268
|
+
vopts = _words(spec.value_options)
|
|
269
|
+
for index, kind in enumerate(POSITIONAL_KINDS.get(name, [])):
|
|
270
|
+
at_position = shlex.quote("{}; and test (__multi_claude_positionals {}) -eq {}".format(
|
|
271
|
+
condition, vopts, index))
|
|
272
|
+
if kind == "name":
|
|
273
|
+
source = "'(multi-claude list --names 2>/dev/null)'"
|
|
274
|
+
elif kind == "dir":
|
|
275
|
+
source = "'(__fish_complete_directories)'"
|
|
276
|
+
elif kind == "file":
|
|
277
|
+
source = "'(__fish_complete_path)'"
|
|
278
|
+
else:
|
|
279
|
+
source = shlex.quote(_words(list(FIXED_WORDS[kind])))
|
|
280
|
+
lines.append("complete -c multi-claude -n {} -a {}".format(at_position, source))
|
|
281
|
+
return "\n".join(lines) + "\n"
|