multi-claude-cli 0.13.0__tar.gz → 0.14.0__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.
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/PKG-INFO +3 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/README.md +2 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/__init__.py +1 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/cli.py +146 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/PKG-INFO +3 -1
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/SOURCES.txt +1 -0
- multi_claude_cli-0.14.0/tests/test_help_examples.py +224 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/LICENSE +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/pyproject.toml +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/setup.cfg +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/__main__.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/accounts.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/actions.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/artifacts.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/bundle.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/completion.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/config.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/doctor.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/env.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/fsutil.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/history.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/hook.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/identity.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/launcher.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/lock.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/migrate.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/plan.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/platform.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/refresh.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/restore.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/routes.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/sessions.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/shared.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/shellpath.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/statusline.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude/usage.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_accounts.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_artifacts.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_bundle.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_completion.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_config.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_doctor.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_everyday.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_fsutil.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_handoff.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_help.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_history.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_identity.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_install.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_mcp.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_migrate.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_onboarding.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_packaging.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_plan.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_platform.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_refresh.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_rename.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_renewal.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_routes.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_sessions.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_set.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_shared_exclude.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_statusline.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_usability.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_usage.py +0 -0
- {multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/tests/test_vscode.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-claude-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Run several Claude Code accounts side by side: separate CLAUDE_CONFIG_DIR directories, launchers, proxies, environment variables and arguments.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/jakoes-wu/multi-claude
|
|
@@ -535,6 +535,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
|
|
|
535
535
|
|
|
536
536
|
### Commands
|
|
537
537
|
|
|
538
|
+
Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
|
|
539
|
+
|
|
538
540
|
| Command | What it does |
|
|
539
541
|
| ---- | ---- |
|
|
540
542
|
| `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
@@ -508,6 +508,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
|
|
|
508
508
|
|
|
509
509
|
### Commands
|
|
510
510
|
|
|
511
|
+
Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
|
|
512
|
+
|
|
511
513
|
| Command | What it does |
|
|
512
514
|
| ---- | ---- |
|
|
513
515
|
| `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
@@ -15,6 +15,7 @@ import shutil
|
|
|
15
15
|
import stat
|
|
16
16
|
import subprocess
|
|
17
17
|
import sys
|
|
18
|
+
import textwrap
|
|
18
19
|
import unicodedata
|
|
19
20
|
from datetime import date, datetime, timedelta, timezone
|
|
20
21
|
from typing import List, NamedTuple, Optional, Tuple
|
|
@@ -110,6 +111,16 @@ def _examples_lines(examples: List[Tuple[str, str]]) -> List[str]:
|
|
|
110
111
|
return lines
|
|
111
112
|
|
|
112
113
|
|
|
114
|
+
def _attach_examples(parser: argparse.ArgumentParser, examples: List[Tuple[str, str]]) -> None:
|
|
115
|
+
"""给子命令帮助末尾加 Examples: 段。示例要靠 RawDescriptionHelpFormatter 才能保留列对齐,
|
|
116
|
+
但它对说明文字也不再折行,所以先把说明按 79 列折好;已手动加过 \\n 的说明是作者自己排过版的,原样保留。
|
|
117
|
+
argparse 在打印帮助时才读这三个属性,子解析器建好后再设置同样生效(方案 feature-help-examples §3)。"""
|
|
118
|
+
if "\n" not in (parser.description or ""):
|
|
119
|
+
parser.description = "\n".join(textwrap.wrap(parser.description or "", 79))
|
|
120
|
+
parser.epilog = "\n".join(_examples_lines(examples))
|
|
121
|
+
parser.formatter_class = argparse.RawDescriptionHelpFormatter
|
|
122
|
+
|
|
123
|
+
|
|
113
124
|
# 子命令帮助末尾的示例:覆盖该命令的全部选项(公共写选项 --verbose 除外),测试逐条解析核对。
|
|
114
125
|
_SET_EXAMPLES = [
|
|
115
126
|
("multi-claude set work --proxy 7901", "set the proxy to http://127.0.0.1:7901"),
|
|
@@ -151,6 +162,138 @@ _RENEWS_LEGEND = [
|
|
|
151
162
|
"(6d ago) passed: update it after the store renews.",
|
|
152
163
|
]
|
|
153
164
|
|
|
165
|
+
# 其余子命令帮助末尾的示例(方案 feature-help-examples §5.1.2):合起来覆盖每个选项与固定取值,照抄即可运行;
|
|
166
|
+
# tests/test_help_examples.py 逐条解析、在沙箱里实跑,并核对没有漏掉的选项。add、set、list 的示例在上面单独定义。
|
|
167
|
+
_SUBCOMMAND_EXAMPLES = {
|
|
168
|
+
"init": [
|
|
169
|
+
("multi-claude init --root ~/.cc --bin-dir ~/.local/bin", "where account directories and launchers go"),
|
|
170
|
+
("multi-claude init --shared-dir ~/.claude-shared", "directory whose items can be linked into accounts"),
|
|
171
|
+
("multi-claude init --shared-items agents,commands,skills,CLAUDE.md", "items to share (this is the default)"),
|
|
172
|
+
("multi-claude init --root ~/.cc --dry-run --verbose", "list every planned action without writing"),
|
|
173
|
+
],
|
|
174
|
+
"migrate-default": [
|
|
175
|
+
("multi-claude migrate-default --dry-run", "preview; without NAME, usually the login e-mail"),
|
|
176
|
+
("multi-claude migrate-default home", "turn ~/.claude into account home, login kept"),
|
|
177
|
+
("multi-claude migrate-default home --proxy 7901", "and give it a proxy"),
|
|
178
|
+
("multi-claude migrate-default home --copy --keep-backup", "copy instead of rename; keep the original"),
|
|
179
|
+
("multi-claude migrate-default home --skip-process-check", "do not check for running Claude Code (risky)"),
|
|
180
|
+
],
|
|
181
|
+
"proxy": [
|
|
182
|
+
("multi-claude proxy work 7901", "same as http://127.0.0.1:7901"),
|
|
183
|
+
("multi-claude proxy work https://proxy.example.com:8443", "any http(s) URL without user or password"),
|
|
184
|
+
("multi-claude proxy work off", "unset the proxy variables in claude-work"),
|
|
185
|
+
("multi-claude proxy work inherit --dry-run", "keep the shell's proxy (default); preview only"),
|
|
186
|
+
],
|
|
187
|
+
"env": [
|
|
188
|
+
("multi-claude env work MAX_THINKING_TOKENS=8000", "add or change a variable"),
|
|
189
|
+
("multi-claude env work --unset MAX_THINKING_TOKENS", "remove it again"),
|
|
190
|
+
("multi-claude env --defaults MAX_THINKING_TOKENS=8000", "for every account"),
|
|
191
|
+
("multi-claude env work DISABLE_TELEMETRY=1 --dry-run", "preview only"),
|
|
192
|
+
],
|
|
193
|
+
"args": [
|
|
194
|
+
("multi-claude args work -- --ide", "claude-work always starts with --ide"),
|
|
195
|
+
("multi-claude args --defaults -- --ide", "the same for every account"),
|
|
196
|
+
("multi-claude args work --", "clear the fixed arguments of work"),
|
|
197
|
+
("multi-claude args work --dry-run -- --ide", "preview only"),
|
|
198
|
+
],
|
|
199
|
+
"remove": [
|
|
200
|
+
("multi-claude remove work", "unregister work; directory and login stay"),
|
|
201
|
+
("multi-claude remove work --dry-run", "preview only"),
|
|
202
|
+
],
|
|
203
|
+
"apply": [
|
|
204
|
+
("multi-claude apply", "rewrite launchers and links to match config.json"),
|
|
205
|
+
("multi-claude apply --dry-run --verbose", "list every planned action without writing"),
|
|
206
|
+
("multi-claude apply -f accounts.json", "adopt accounts.json as the new configuration"),
|
|
207
|
+
],
|
|
208
|
+
"usage": [
|
|
209
|
+
("multi-claude usage", "5-hour and 7-day usage of every account"),
|
|
210
|
+
("multi-claude usage work --json", "one account, for scripts"),
|
|
211
|
+
("multi-claude usage --history", "tokens per day, last 7 days"),
|
|
212
|
+
("multi-claude usage --history work --days 30 --by model", "one account, 30 days, per model"),
|
|
213
|
+
("multi-claude usage --history --by day", "per day (the default grouping)"),
|
|
214
|
+
],
|
|
215
|
+
"doctor": [
|
|
216
|
+
("multi-claude doctor", "check the setup and suggest fixes"),
|
|
217
|
+
("multi-claude doctor --verbose", "also list the checks that passed"),
|
|
218
|
+
("multi-claude doctor --json", "for scripts"),
|
|
219
|
+
],
|
|
220
|
+
"route": [
|
|
221
|
+
("multi-claude route ~/work work", "claude-auto uses work under ~/work"),
|
|
222
|
+
("multi-claude route ~/work --remove", "remove that route"),
|
|
223
|
+
("multi-claude route --default home", "use home where no route matches"),
|
|
224
|
+
("multi-claude route --no-default", "run plain claude where no route matches"),
|
|
225
|
+
("multi-claude route ~/work work --dry-run", "preview only"),
|
|
226
|
+
],
|
|
227
|
+
"rename": [
|
|
228
|
+
("multi-claude rename work job", "claude-work becomes claude-job"),
|
|
229
|
+
("multi-claude rename work job --dry-run", "preview only"),
|
|
230
|
+
],
|
|
231
|
+
"mcp": [
|
|
232
|
+
("multi-claude mcp work list", "the MCP servers of work"),
|
|
233
|
+
("multi-claude mcp work add --scope user github -- npx -y @modelcontextprotocol/server-github", "everything after the name goes to `claude mcp`"),
|
|
234
|
+
],
|
|
235
|
+
"login": [
|
|
236
|
+
("multi-claude login work", "runs `claude auth login` as work"),
|
|
237
|
+
],
|
|
238
|
+
"which": [
|
|
239
|
+
("multi-claude which", "the account claude-auto would use here"),
|
|
240
|
+
("multi-claude which ~/work", "... in ~/work"),
|
|
241
|
+
],
|
|
242
|
+
"run": [
|
|
243
|
+
("multi-claude run work", "same as claude-work"),
|
|
244
|
+
("multi-claude run work -- claude --version", "any command, with the environment of claude-work"),
|
|
245
|
+
("multi-claude run -- env", "without a name: the account claude-auto would use here"),
|
|
246
|
+
],
|
|
247
|
+
"path": [
|
|
248
|
+
("multi-claude path work", "print the directory of work"),
|
|
249
|
+
],
|
|
250
|
+
"code": [
|
|
251
|
+
("multi-claude code work ~/work", "a separate VS Code window for work"),
|
|
252
|
+
("multi-claude code work . -- --disable-gpu", "arguments after -- go to VS Code"),
|
|
253
|
+
],
|
|
254
|
+
"restore": [
|
|
255
|
+
("multi-claude restore home --dry-run", "preview moving home back to ~/.claude"),
|
|
256
|
+
("multi-claude restore home", "undo migrate-default, login kept"),
|
|
257
|
+
("multi-claude restore home --skip-process-check", "do not check for running Claude Code (risky)"),
|
|
258
|
+
],
|
|
259
|
+
"export": [
|
|
260
|
+
("multi-claude export work ~/work-settings.tar.gz", "settings, CLAUDE.md, agents, skills, MCP; no logins"),
|
|
261
|
+
],
|
|
262
|
+
"import": [
|
|
263
|
+
("multi-claude import ~/work-settings.tar.gz home", "into an existing account"),
|
|
264
|
+
("multi-claude import ~/work-settings.tar.gz home --dry-run", "preview only"),
|
|
265
|
+
("multi-claude import ~/work-settings.tar.gz home --force", "back up and replace items that differ"),
|
|
266
|
+
],
|
|
267
|
+
"completion": [
|
|
268
|
+
("multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude", "bash completion for your user"),
|
|
269
|
+
('multi-claude completion zsh > "${fpath[1]}/_multi-claude"', "zsh: any directory on your fpath"),
|
|
270
|
+
("multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish", "fish completion"),
|
|
271
|
+
],
|
|
272
|
+
"statusline": [
|
|
273
|
+
("multi-claude statusline install ~/.claude/settings.json", "record fresh usage from the status line"),
|
|
274
|
+
("multi-claude statusline uninstall ~/.claude/settings.json --dry-run", "preview restoring the original command"),
|
|
275
|
+
],
|
|
276
|
+
"handoff": [
|
|
277
|
+
("! multi-claude handoff home", "inside a Claude session: hand it to home"),
|
|
278
|
+
("multi-claude handoff home --from work", "the latest session of work in this directory"),
|
|
279
|
+
("multi-claude handoff home --from work --session 3c588340-1219-48df-a231-509866789487", "a given session"),
|
|
280
|
+
("multi-claude handoff home --from work --force --dry-run", "preview replacing a different copy"),
|
|
281
|
+
],
|
|
282
|
+
"sessions": [
|
|
283
|
+
("multi-claude sessions", "the 30 newest sessions of every account"),
|
|
284
|
+
("multi-claude sessions work --limit 10", "one account, 10 sessions"),
|
|
285
|
+
("multi-claude sessions --grep importer --cmd", "with the command that resumes each"),
|
|
286
|
+
("multi-claude sessions --limit 0 --json", "every session, for scripts"),
|
|
287
|
+
("multi-claude conversations --grep importer", "another name for sessions"),
|
|
288
|
+
],
|
|
289
|
+
"artifacts": [
|
|
290
|
+
("multi-claude artifacts", "the 30 newest artifacts of every account"),
|
|
291
|
+
("multi-claude artifacts work --grep report", "one account, matching text"),
|
|
292
|
+
("multi-claude artifacts --path --cmd", "with the source file and the resume command"),
|
|
293
|
+
("multi-claude artifacts --limit 0 --json", "every artifact, for scripts"),
|
|
294
|
+
],
|
|
295
|
+
}
|
|
296
|
+
|
|
154
297
|
|
|
155
298
|
def build_parser() -> argparse.ArgumentParser:
|
|
156
299
|
# 子命令不传 help=,argparse 就不会把它们平铺成一列;改由 epilog 按用途分组列出(方案 feature-clearer-help §5.1.1)。
|
|
@@ -371,6 +514,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
371
514
|
help="also print, below each artifact, the local file it was published from, marked "
|
|
372
515
|
"(missing) when it no longer exists")
|
|
373
516
|
p_artifacts.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
|
517
|
+
# add、set、list 的示例在 add_parser 时已传入;其余子命令在这里统一挂上(键必须是已定义的子命令名)。
|
|
518
|
+
for name, examples in _SUBCOMMAND_EXAMPLES.items():
|
|
519
|
+
_attach_examples(sub.choices[name], examples)
|
|
374
520
|
return parser
|
|
375
521
|
|
|
376
522
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-claude-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Run several Claude Code accounts side by side: separate CLAUDE_CONFIG_DIR directories, launchers, proxies, environment variables and arguments.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/jakoes-wu/multi-claude
|
|
@@ -535,6 +535,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
|
|
|
535
535
|
|
|
536
536
|
### Commands
|
|
537
537
|
|
|
538
|
+
Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
|
|
539
|
+
|
|
538
540
|
| Command | What it does |
|
|
539
541
|
| ---- | ---- |
|
|
540
542
|
| `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""方案 feature-help-examples §8:每个子命令的帮助都有示例,示例能解析、能运行、覆盖全部选项。"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shlex
|
|
10
|
+
import shutil
|
|
11
|
+
import sys
|
|
12
|
+
import unittest
|
|
13
|
+
|
|
14
|
+
from helpers import CliTestCase, SRC
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, SRC)
|
|
17
|
+
|
|
18
|
+
from multi_claude import cli # noqa: E402
|
|
19
|
+
|
|
20
|
+
# 示例里出现这些 shell 运算符时,之后的词交给 shell,不是 multi-claude 的参数。
|
|
21
|
+
SHELL_OPERATORS = (">", ">>", "|")
|
|
22
|
+
|
|
23
|
+
# E3:这两个命令的 --verbose 是自己的功能;其余命令的 --verbose 来自 _add_write_options,是公共写选项,不要求示例覆盖。
|
|
24
|
+
OWN_VERBOSE = ("list", "doctor")
|
|
25
|
+
|
|
26
|
+
# E5 跳过的命令及理由(方案 §8.1 第 5 条);它们的示例仍由 E2 解析核对。
|
|
27
|
+
RUN_SKIPPED = {
|
|
28
|
+
"list": "looks up plans over the network; its examples are parsed by test_renewal T8",
|
|
29
|
+
"handoff": "needs a Claude session, or a real session in the source account",
|
|
30
|
+
"migrate-default": "needs a real ~/.claude and the login e-mail in ~/.claude.json",
|
|
31
|
+
"restore": "needs an account migrated from ~/.claude",
|
|
32
|
+
"code": "would start the real VS Code found in PATH; covered by test_vscode with a fake code",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# E5 允许的非零退出码:示例 → (退出码, 原因)。只收沙箱环境造成的失败,退出码 2(参数被拒)一律不允许。
|
|
36
|
+
ALLOWED_EXIT = {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def subparsers():
|
|
40
|
+
parser = cli.build_parser()
|
|
41
|
+
for action in parser._actions:
|
|
42
|
+
if isinstance(action, argparse._SubParsersAction):
|
|
43
|
+
return action.choices
|
|
44
|
+
raise AssertionError("no subparsers")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def help_text(name):
|
|
48
|
+
out = io.StringIO()
|
|
49
|
+
with contextlib.redirect_stdout(out):
|
|
50
|
+
try:
|
|
51
|
+
cli.build_parser().parse_args([name, "--help"])
|
|
52
|
+
except SystemExit as exc:
|
|
53
|
+
if exc.code != 0:
|
|
54
|
+
raise AssertionError("{} --help exited {}".format(name, exc.code))
|
|
55
|
+
return out.getvalue()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def example_commands(text):
|
|
59
|
+
"""帮助文本 Examples: 段里每行的命令列(与说明之间至少两个空格)。"""
|
|
60
|
+
if "Examples:" not in text:
|
|
61
|
+
return []
|
|
62
|
+
commands = []
|
|
63
|
+
for line in text.split("Examples:", 1)[1].splitlines()[1:]:
|
|
64
|
+
if not line.startswith(" "):
|
|
65
|
+
break
|
|
66
|
+
commands.append(re.split(r"\s{2,}", line.strip())[0])
|
|
67
|
+
return commands
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def example_words(command):
|
|
71
|
+
"""示例命令切成 multi-claude 之后的参数:去掉会话里运行用的 `! `,截掉 shell 运算符之后的部分。"""
|
|
72
|
+
words = shlex.split(command)
|
|
73
|
+
if words[:1] == ["!"]:
|
|
74
|
+
words = words[1:]
|
|
75
|
+
if words[:1] != ["multi-claude"]:
|
|
76
|
+
raise AssertionError("example does not start with multi-claude: {}".format(command))
|
|
77
|
+
words = words[1:]
|
|
78
|
+
for index, word in enumerate(words):
|
|
79
|
+
if word in SHELL_OPERATORS:
|
|
80
|
+
return words[:index]
|
|
81
|
+
return words
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_like_main(words):
|
|
85
|
+
"""走 main 在 argparse 之前的同一串步骤(方案 §5.1.3),返回解析结果;--help 示例返回 None。"""
|
|
86
|
+
parser = cli.build_parser()
|
|
87
|
+
argv = cli._resolve_command_alias(list(words))
|
|
88
|
+
problem = cli._unknown_command(parser, argv) or cli._misplaced_account_name(argv)
|
|
89
|
+
if problem is not None:
|
|
90
|
+
raise AssertionError(problem)
|
|
91
|
+
argv, _launch_args = cli._split_args_command(argv)
|
|
92
|
+
if "--help" in argv or "-h" in argv:
|
|
93
|
+
return None
|
|
94
|
+
return parser.parse_args(argv)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class HelpExamplesTest(unittest.TestCase):
|
|
98
|
+
def test_e1_every_subcommand_has_examples(self):
|
|
99
|
+
names = list(subparsers())
|
|
100
|
+
self.assertEqual(len(names), 28)
|
|
101
|
+
for name in names:
|
|
102
|
+
with self.subTest(command=name):
|
|
103
|
+
self.assertTrue(example_commands(help_text(name)))
|
|
104
|
+
|
|
105
|
+
def test_e2_examples_parse_like_main(self):
|
|
106
|
+
for name in subparsers():
|
|
107
|
+
for command in example_commands(help_text(name)):
|
|
108
|
+
with self.subTest(command=command):
|
|
109
|
+
args = parse_like_main(example_words(command))
|
|
110
|
+
if args is not None:
|
|
111
|
+
# 防止把别的命令的示例贴进这个命令的帮助。
|
|
112
|
+
self.assertEqual(args.command, name)
|
|
113
|
+
|
|
114
|
+
def test_e3_examples_cover_every_option_and_choice(self):
|
|
115
|
+
for name, parser in subparsers().items():
|
|
116
|
+
with self.subTest(command=name):
|
|
117
|
+
# 按整词比较:子串匹配会让 --shared-exclude 冒充 --shared 已覆盖。
|
|
118
|
+
used = {word for command in example_commands(help_text(name)) for word in example_words(command)}
|
|
119
|
+
for action in parser._actions:
|
|
120
|
+
if "-h" in action.option_strings:
|
|
121
|
+
continue
|
|
122
|
+
if action.option_strings == ["--verbose"] and name not in OWN_VERBOSE:
|
|
123
|
+
continue
|
|
124
|
+
if action.option_strings:
|
|
125
|
+
self.assertTrue(set(action.option_strings) & used,
|
|
126
|
+
"{} {} has no example".format(name, action.option_strings))
|
|
127
|
+
for choice in action.choices or ():
|
|
128
|
+
self.assertIn(choice, used, "{} value {!r} has no example".format(name, choice))
|
|
129
|
+
|
|
130
|
+
def test_e4_descriptions_and_example_columns(self):
|
|
131
|
+
for name, parser in subparsers().items():
|
|
132
|
+
with self.subTest(command=name):
|
|
133
|
+
for line in (parser.description or "").splitlines():
|
|
134
|
+
self.assertLessEqual(len(line), 80)
|
|
135
|
+
lines = help_text(name).split("Examples:", 1)[1].splitlines()[1:]
|
|
136
|
+
lines = lines[:next((i for i, line in enumerate(lines) if not line.startswith(" ")), len(lines))]
|
|
137
|
+
# 说明列对齐、且与最长的命令至少隔两个空格;只看拆分结果查不出“只隔一个空格”。
|
|
138
|
+
widest = max(len(re.split(r"\s{2,}", line.strip())[0]) for line in lines)
|
|
139
|
+
starts = {len(line) - len(re.split(r"\s{2,}", line.strip())[-1]) for line in lines
|
|
140
|
+
if re.search(r"\S\s{2,}\S", line)}
|
|
141
|
+
self.assertEqual(len(starts), 1, lines)
|
|
142
|
+
self.assertGreaterEqual(starts.pop(), 2 + widest + 2, lines)
|
|
143
|
+
|
|
144
|
+
def test_e6_existing_help_untouched(self):
|
|
145
|
+
# add、set、list 与主帮助的示例在本方案之前就有,统一挂示例的循环不得改动它们。
|
|
146
|
+
self.assertFalse({"add", "set", "list"} & set(cli._SUBCOMMAND_EXAMPLES))
|
|
147
|
+
parsers = subparsers()
|
|
148
|
+
expected = {
|
|
149
|
+
"add": "\n".join(cli._examples_lines(cli._ADD_EXAMPLES)),
|
|
150
|
+
"set": "\n".join(cli._examples_lines(cli._SET_EXAMPLES)),
|
|
151
|
+
"list": "\n".join(cli._examples_lines(cli._LIST_EXAMPLES) + [""] + cli._RENEWS_LEGEND),
|
|
152
|
+
}
|
|
153
|
+
for name, epilog in expected.items():
|
|
154
|
+
with self.subTest(command=name):
|
|
155
|
+
self.assertEqual(parsers[name].epilog, epilog)
|
|
156
|
+
self.assertIs(parsers[name].formatter_class, argparse.RawDescriptionHelpFormatter)
|
|
157
|
+
self.assertEqual(parsers["add"].description,
|
|
158
|
+
"add an account, adopt an existing directory, or change its options")
|
|
159
|
+
self.assertEqual(parsers["set"].description, "change an existing account: proxy, sharing and renewal date\n"
|
|
160
|
+
"(use env, args, rename for the rest)")
|
|
161
|
+
self.assertEqual(parsers["list"].description, "show accounts and their status; looks up each account's plan\n"
|
|
162
|
+
"and billing channel from Anthropic unless --offline is given")
|
|
163
|
+
main = cli.build_parser()
|
|
164
|
+
self.assertEqual(main.epilog, cli._help_epilog())
|
|
165
|
+
self.assertEqual(main.description,
|
|
166
|
+
"Manage multiple Claude Code accounts: separate CLAUDE_CONFIG_DIR directories,\n"
|
|
167
|
+
"per-account launchers, proxies, environment variables and arguments.")
|
|
168
|
+
self.assertIs(main.formatter_class, argparse.RawDescriptionHelpFormatter)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class HelpExamplesRunTest(CliTestCase):
|
|
172
|
+
"""E5(方案 §8.1):在沙箱里以子进程真正运行示例,确认照抄能用,不只是能解析。"""
|
|
173
|
+
|
|
174
|
+
def prepare(self, name):
|
|
175
|
+
self.ok("add", "work")
|
|
176
|
+
self.ok("add", "home")
|
|
177
|
+
os.makedirs(os.path.join(self.home, "work"))
|
|
178
|
+
if name == "run":
|
|
179
|
+
# 不给账号名的 `run -- env` 要靠路由或默认账号选账号,否则以用法错误退出。
|
|
180
|
+
self.ok("route", "--default", "home")
|
|
181
|
+
elif name == "import":
|
|
182
|
+
# 只在 import 沙箱预先导出:export 沙箱若也有这个文件,export 示例会因目标已存在而失败。
|
|
183
|
+
self.ok("export", "work", os.path.join(self.home, "work-settings.tar.gz"))
|
|
184
|
+
elif name == "apply":
|
|
185
|
+
shutil.copy(os.path.join(self.state, "config.json"), os.path.join(self.home, "accounts.json"))
|
|
186
|
+
elif name == "add":
|
|
187
|
+
# add work --config-from home 要从 home 复制 settings.json。
|
|
188
|
+
self.write(os.path.join(self.root, "home", "settings.json"), "{}")
|
|
189
|
+
elif name == "statusline":
|
|
190
|
+
# install 要在 PATH 里找到 multi-claude;install 与 uninstall 都要读一份带 command 类型 statusLine 的设置文件。
|
|
191
|
+
fake = self.write(os.path.join(self.fakebin, "multi-claude"), "#!/bin/sh\nexit 0\n")
|
|
192
|
+
os.chmod(fake, 0o755)
|
|
193
|
+
self.write(os.path.join(self.home, ".claude", "settings.json"),
|
|
194
|
+
json.dumps({"statusLine": {"type": "command", "command": "echo usage"}}))
|
|
195
|
+
|
|
196
|
+
def run_example(self, command):
|
|
197
|
+
# shlex 不展开 ~,multi-claude 的部分命令也不展开,这里按 shell 的规则展开到沙箱的 home。
|
|
198
|
+
words = [self.home + word[1:] if word == "~" or word.startswith("~/") else word
|
|
199
|
+
for word in example_words(command)]
|
|
200
|
+
return self.run_cli(*words, cwd=self.home)
|
|
201
|
+
|
|
202
|
+
def check_examples_run(self, name):
|
|
203
|
+
commands = example_commands(help_text(name))
|
|
204
|
+
if name != "statusline":
|
|
205
|
+
# 先跑预览:rename / remove 真改之后,同命令的 --dry-run 示例会找不到账号。
|
|
206
|
+
# statusline 例外,按顺序跑,uninstall 的预览才是 install 之后的还原。
|
|
207
|
+
commands = [c for c in commands if "--dry-run" in c] + [c for c in commands if "--dry-run" not in c]
|
|
208
|
+
self.prepare(name)
|
|
209
|
+
for command in commands:
|
|
210
|
+
with self.subTest(command=command):
|
|
211
|
+
result = self.run_example(command)
|
|
212
|
+
expected = ALLOWED_EXIT.get(command, (0, ""))[0]
|
|
213
|
+
self.assertNotEqual(result.code, 2, result)
|
|
214
|
+
self.assertEqual(result.code, expected, result)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# 每个子命令一个测试方法,各自一个新沙箱:前一个命令的示例不会改掉后一个命令依赖的账号与文件。
|
|
218
|
+
for _name in subparsers():
|
|
219
|
+
if _name not in RUN_SKIPPED:
|
|
220
|
+
setattr(HelpExamplesRunTest, "test_e5_" + _name.replace("-", "_"),
|
|
221
|
+
lambda self, name=_name: self.check_examples_run(name))
|
|
222
|
+
|
|
223
|
+
if __name__ == "__main__":
|
|
224
|
+
unittest.main()
|
|
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
|
|
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
|
{multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{multi_claude_cli-0.13.0 → multi_claude_cli-0.14.0}/src/multi_claude_cli.egg-info/top_level.txt
RENAMED
|
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
|
|
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
|