multi-claude-cli 0.5.0__tar.gz → 0.6.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.5.0/src/multi_claude_cli.egg-info → multi_claude_cli-0.6.0}/PKG-INFO +19 -2
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/README.md +18 -1
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/__init__.py +1 -1
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/cli.py +73 -8
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/completion.py +1 -1
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0/src/multi_claude_cli.egg-info}/PKG-INFO +19 -2
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/SOURCES.txt +2 -1
- multi_claude_cli-0.6.0/tests/test_vscode.py +127 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/LICENSE +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/pyproject.toml +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/setup.cfg +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/__main__.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/accounts.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/actions.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/config.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/doctor.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/env.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/fsutil.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/hook.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/identity.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/launcher.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/lock.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/migrate.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/platform.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/restore.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/routes.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/sessions.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/shared.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/shellpath.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/statusline.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/usage.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_accounts.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_completion.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_config.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_doctor.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_everyday.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_fsutil.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_handoff.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_help.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_identity.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_install.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_mcp.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_migrate.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_onboarding.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_packaging.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_platform.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_rename.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_routes.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_set.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_shared_exclude.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_statusline.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_usability.py +0 -0
- {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_usage.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.6.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
|
|
@@ -286,6 +286,22 @@ multi-claude add client-b --config-from work # start client-b with a copy of wor
|
|
|
286
286
|
|
|
287
287
|
`--config-from` copies `settings.json` once; afterwards the two files are independent. A different existing `settings.json` in the new account, or a `settings.json` that the account shares, is a conflict and nothing is changed. Use [shared resources](#shared-resources) for settings that should stay the same in every account.
|
|
288
288
|
|
|
289
|
+
### Opening VS Code for an account (experimental)
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
multi-claude code work ~/projects/app # a separate VS Code window for work
|
|
293
|
+
multi-claude code personal . -- --disable-gpu # arguments after -- go to VS Code
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The Claude Code extension uses the `CLAUDE_CONFIG_DIR` of the environment VS Code was started with, so a VS Code opened from the Dock always uses `~/.claude`. `code` starts VS Code with the account's environment (the same as `run`) and a separate user data directory, `<root>/.apps/<account directory>/vscode`. A separate user data directory is required: otherwise VS Code hands the request to the instance that is already running, with its own environment. Each account's instance starts with default VS Code settings; extensions are shared because they live in `~/.vscode/extensions`.
|
|
297
|
+
|
|
298
|
+
Notes:
|
|
299
|
+
|
|
300
|
+
- If an instance for the account is already running, the request goes to it; changes to the proxy or environment variables apply after that instance quits.
|
|
301
|
+
- Setting `CLAUDE_CONFIG_DIR` in the extension's `claudeCode.environmentVariables` inside that instance overrides the account.
|
|
302
|
+
- On macOS the `code` command passes the whole environment to `open --env`, so the values are briefly visible in the process list.
|
|
303
|
+
- Verified with VS Code 1.139.1 and Claude Code extension 2.1.286 on macOS; `code` prints a warning because it relies on undocumented behaviour. It needs the `code` command on `PATH` (in VS Code: "Shell Command: Install 'code' command in PATH").
|
|
304
|
+
|
|
289
305
|
### Diagnostics and completion
|
|
290
306
|
|
|
291
307
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
@@ -389,6 +405,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
389
405
|
| `multi-claude mcp NAME [ARG ...]` | Run `claude mcp ARG ...` as that account. |
|
|
390
406
|
| `multi-claude run [NAME] [-- COMMAND ...]` | Run a command (default: `claude` with the fixed arguments) with the environment of the account's launcher. Without NAME, the account `claude-auto` would use. |
|
|
391
407
|
| `multi-claude path NAME` | Print the account's directory. |
|
|
408
|
+
| `multi-claude code NAME [PATH] [-- ARGS ...]` | Open a separate VS Code instance with the account's environment (experimental). |
|
|
392
409
|
| `multi-claude handoff TARGET [--from NAME] [--session ID] [--force]` | Copy a session to another account and print the command that resumes it there. |
|
|
393
410
|
| `multi-claude statusline install\|uninstall FILE` | Wrap the statusLine command in a settings file so that `usage` gets fresh numbers, or restore it. |
|
|
394
411
|
| `multi-claude completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
@@ -464,7 +481,7 @@ If you move an account directory yourself, its launcher reports that the directo
|
|
|
464
481
|
./install.sh --uninstall
|
|
465
482
|
```
|
|
466
483
|
|
|
467
|
-
This removes the tool only. Your configuration, account directories, logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
484
|
+
This removes the tool only. Your configuration, account directories (including the VS Code data in `<root>/.apps` created by `code`), logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
468
485
|
|
|
469
486
|
## Contributing
|
|
470
487
|
|
|
@@ -259,6 +259,22 @@ multi-claude add client-b --config-from work # start client-b with a copy of wor
|
|
|
259
259
|
|
|
260
260
|
`--config-from` copies `settings.json` once; afterwards the two files are independent. A different existing `settings.json` in the new account, or a `settings.json` that the account shares, is a conflict and nothing is changed. Use [shared resources](#shared-resources) for settings that should stay the same in every account.
|
|
261
261
|
|
|
262
|
+
### Opening VS Code for an account (experimental)
|
|
263
|
+
|
|
264
|
+
```sh
|
|
265
|
+
multi-claude code work ~/projects/app # a separate VS Code window for work
|
|
266
|
+
multi-claude code personal . -- --disable-gpu # arguments after -- go to VS Code
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The Claude Code extension uses the `CLAUDE_CONFIG_DIR` of the environment VS Code was started with, so a VS Code opened from the Dock always uses `~/.claude`. `code` starts VS Code with the account's environment (the same as `run`) and a separate user data directory, `<root>/.apps/<account directory>/vscode`. A separate user data directory is required: otherwise VS Code hands the request to the instance that is already running, with its own environment. Each account's instance starts with default VS Code settings; extensions are shared because they live in `~/.vscode/extensions`.
|
|
270
|
+
|
|
271
|
+
Notes:
|
|
272
|
+
|
|
273
|
+
- If an instance for the account is already running, the request goes to it; changes to the proxy or environment variables apply after that instance quits.
|
|
274
|
+
- Setting `CLAUDE_CONFIG_DIR` in the extension's `claudeCode.environmentVariables` inside that instance overrides the account.
|
|
275
|
+
- On macOS the `code` command passes the whole environment to `open --env`, so the values are briefly visible in the process list.
|
|
276
|
+
- Verified with VS Code 1.139.1 and Claude Code extension 2.1.286 on macOS; `code` prints a warning because it relies on undocumented behaviour. It needs the `code` command on `PATH` (in VS Code: "Shell Command: Install 'code' command in PATH").
|
|
277
|
+
|
|
262
278
|
### Diagnostics and completion
|
|
263
279
|
|
|
264
280
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
@@ -362,6 +378,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
362
378
|
| `multi-claude mcp NAME [ARG ...]` | Run `claude mcp ARG ...` as that account. |
|
|
363
379
|
| `multi-claude run [NAME] [-- COMMAND ...]` | Run a command (default: `claude` with the fixed arguments) with the environment of the account's launcher. Without NAME, the account `claude-auto` would use. |
|
|
364
380
|
| `multi-claude path NAME` | Print the account's directory. |
|
|
381
|
+
| `multi-claude code NAME [PATH] [-- ARGS ...]` | Open a separate VS Code instance with the account's environment (experimental). |
|
|
365
382
|
| `multi-claude handoff TARGET [--from NAME] [--session ID] [--force]` | Copy a session to another account and print the command that resumes it there. |
|
|
366
383
|
| `multi-claude statusline install\|uninstall FILE` | Wrap the statusLine command in a settings file so that `usage` gets fresh numbers, or restore it. |
|
|
367
384
|
| `multi-claude completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
@@ -437,7 +454,7 @@ If you move an account directory yourself, its launcher reports that the directo
|
|
|
437
454
|
./install.sh --uninstall
|
|
438
455
|
```
|
|
439
456
|
|
|
440
|
-
This removes the tool only. Your configuration, account directories, logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
457
|
+
This removes the tool only. Your configuration, account directories (including the VS Code data in `<root>/.apps` created by `code`), logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
441
458
|
|
|
442
459
|
## Contributing
|
|
443
460
|
|
|
@@ -56,6 +56,7 @@ COMMAND_GROUPS = [
|
|
|
56
56
|
("handoff", "copy a session to another account"),
|
|
57
57
|
("mcp", "run `claude mcp` as an account"),
|
|
58
58
|
("run, path", "run any command as an account; print its directory"),
|
|
59
|
+
("code", "open VS Code for an account (experimental)"),
|
|
59
60
|
]),
|
|
60
61
|
("Setup and checks", [
|
|
61
62
|
("init, apply", "global settings; converge everything to config.json"),
|
|
@@ -217,6 +218,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
217
218
|
p_path = sub.add_parser("path", description="print the directory of an account")
|
|
218
219
|
p_path.add_argument("name")
|
|
219
220
|
|
|
221
|
+
# code 的 VS Code 参数与 run 一样,在 argparse 之前从第一个 `--` 处切走。
|
|
222
|
+
p_code = sub.add_parser("code", description="open a separate VS Code instance with the environment of an "
|
|
223
|
+
"account (experimental): code NAME [PATH] [-- CODE_ARGS ...]")
|
|
224
|
+
p_code.add_argument("name")
|
|
225
|
+
p_code.add_argument("path", nargs="?", metavar="PATH")
|
|
226
|
+
|
|
220
227
|
p_restore = sub.add_parser("restore", description="undo migrate-default: move the account directory back to "
|
|
221
228
|
"~/.claude and unregister the account; the login is kept")
|
|
222
229
|
p_restore.add_argument("name")
|
|
@@ -285,7 +292,7 @@ def _add_write_options(parser: argparse.ArgumentParser) -> None:
|
|
|
285
292
|
def _split_args_command(argv: List[str]) -> Tuple[List[str], Optional[List[str]]]:
|
|
286
293
|
"""子命令是 `args` 时,把第一个 `--` 之后的全部内容取出来作为参数列表;
|
|
287
294
|
子命令是 `mcp` 或 `login` 时,账号名之后的全部内容原样作为 `claude mcp` / `claude auth login` 的参数(不经 argparse);
|
|
288
|
-
子命令是 `run` 时,第一个 `--`
|
|
295
|
+
子命令是 `run` / `code` 时,第一个 `--` 之后的内容是要运行的命令 / 传给 VS Code 的参数。
|
|
289
296
|
|
|
290
297
|
不能用 argparse.REMAINDER:可选位置参数加 --defaults 加 REMAINDER 时,
|
|
291
298
|
`args --defaults -- --settings s` 会被解析成账号名 `--settings`(方案 §5.1.2,实测 3.10/3.11/3.13)。
|
|
@@ -307,8 +314,8 @@ def _split_args_command(argv: List[str]) -> Tuple[List[str], Optional[List[str]]
|
|
|
307
314
|
example = "multi-claude mcp NAME list" if command == "mcp" else "multi-claude login NAME"
|
|
308
315
|
raise UsageError("{} needs an account name first, e.g. `{}`".format(command, example))
|
|
309
316
|
return argv[:command_index + 2], rest[1:]
|
|
310
|
-
if command_index is not None and argv[command_index]
|
|
311
|
-
# run
|
|
317
|
+
if command_index is not None and argv[command_index] in ("run", "code"):
|
|
318
|
+
# run 的命令(code 的 VS Code 参数)从第一个 `--` 之后原样取出;没有 `--` 与 `--` 后为空都表示“没有”。
|
|
312
319
|
if "--" not in argv[command_index + 1:]:
|
|
313
320
|
return argv, None
|
|
314
321
|
separator = argv.index("--", command_index + 1)
|
|
@@ -394,7 +401,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
394
401
|
return accounts.EXIT_ERROR
|
|
395
402
|
# 不加写锁:凭据由 Claude 自己写入,登录过程需要浏览器交互,可能持续很久。
|
|
396
403
|
return cmd_login(args.name, args.launch_args or [])
|
|
397
|
-
if args.command in ("run", "path"):
|
|
404
|
+
if args.command in ("run", "path", "code"):
|
|
398
405
|
if notice:
|
|
399
406
|
# 与 mcp 相同:迁移进行到一半时账号目录可能正在搬动。
|
|
400
407
|
error("finish the unfinished migration before running `{}`".format(args.command))
|
|
@@ -402,6 +409,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
402
409
|
# 只读配置、不加写锁:run 会把进程换成目标命令,可能运行很久。
|
|
403
410
|
if args.command == "path":
|
|
404
411
|
return cmd_path(args.name)
|
|
412
|
+
if args.command == "code":
|
|
413
|
+
return cmd_code(args.name, args.path, args.launch_args or [])
|
|
405
414
|
return cmd_run(args.name, args.launch_args or [])
|
|
406
415
|
if args.command == "mcp":
|
|
407
416
|
if notice:
|
|
@@ -831,14 +840,22 @@ def cmd_run(name: Optional[str], command: List[str]) -> int:
|
|
|
831
840
|
# 提示写 stderr:stdout 留给目标命令,`run -- cmd | …` 才不会混进这一行。
|
|
832
841
|
print("[multi-claude] run: using account {} ({})".format(
|
|
833
842
|
account.name, "route " + rule.path if rule is not None else "default"), file=sys.stderr)
|
|
834
|
-
process_env, code = _usable_account_env(config, account)
|
|
835
|
-
if process_env is None:
|
|
836
|
-
return code
|
|
837
843
|
if not command:
|
|
838
844
|
# 固定参数按启动命令的规则展开 `~`(launcher.render),两种启动方式收到的参数才逐项相同。
|
|
839
845
|
command = ["claude"] + [launcher.expand_value(arg) for arg in config.effective_args(account)]
|
|
846
|
+
return _exec_as(config, account, command)
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _exec_as(config: Config, account: Account, command: List[str], not_found_hint: str = "") -> int:
|
|
850
|
+
"""run 与 code 共用:账号可用时以它的启动命令环境 exec 目标命令,成功则不返回。
|
|
851
|
+
|
|
852
|
+
返回值只出现在失败时:账号不可用 1,PATH 中找不到命令 127。
|
|
853
|
+
"""
|
|
854
|
+
process_env, code = _usable_account_env(config, account)
|
|
855
|
+
if process_env is None:
|
|
856
|
+
return code
|
|
840
857
|
if shutil.which(command[0], path=process_env.get("PATH")) is None:
|
|
841
|
-
error("{} not found in PATH".format(command[0]))
|
|
858
|
+
error("{} not found in PATH{}".format(command[0], not_found_hint))
|
|
842
859
|
return 127
|
|
843
860
|
sys.stdout.flush()
|
|
844
861
|
sys.stderr.flush()
|
|
@@ -847,6 +864,54 @@ def cmd_run(name: Optional[str], command: List[str]) -> int:
|
|
|
847
864
|
return accounts.EXIT_ERROR # 不会执行到这里:execvpe 成功后不返回
|
|
848
865
|
|
|
849
866
|
|
|
867
|
+
# code 依赖的未公开行为由 2026-10-02 实测确认的版本(方案 feature-vscode-launch §1.1)。
|
|
868
|
+
VSCODE_VERIFIED_WITH = "VS Code 1.139.1, extension 2.1.286"
|
|
869
|
+
# macOS 的 unix 套接字路径上限是 104 字节,VS Code 在 macOS 上把主套接字放在用户数据目录下(形如 1.13-main.sock,
|
|
870
|
+
# 约 15 字节,见 VS Code out/main.js 构造套接字路径的函数);Linux 设了 XDG_RUNTIME_DIR 时不放在这里。
|
|
871
|
+
_SOCKET_DIR_WARN_LENGTH = 80
|
|
872
|
+
_CODE_NOT_FOUND_HINT = "; in VS Code run \"Shell Command: Install 'code' command in PATH\""
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def cmd_code(name: str, path: Optional[str], code_args: List[str]) -> int:
|
|
876
|
+
"""以账号环境打开一个独立的 VS Code 实例(实验功能,方案 feature-vscode-launch §5.1)。
|
|
877
|
+
|
|
878
|
+
必须给每个账号单独的 --user-data-dir:VS Code 已在运行时,同一用户数据目录的请求会转给已运行的实例,
|
|
879
|
+
新窗口沿用那个实例的环境,账号就不生效了。目录按账号目录名存放,rename 后数据仍在原处。
|
|
880
|
+
"""
|
|
881
|
+
warn("experimental: code relies on undocumented behaviour of VS Code and the Claude Code extension "
|
|
882
|
+
"(verified with {})".format(VSCODE_VERIFIED_WITH))
|
|
883
|
+
config, _ = load_config()
|
|
884
|
+
account = config.find(_checked_name(name))
|
|
885
|
+
if account is None:
|
|
886
|
+
error("account {!r} is not registered".format(name))
|
|
887
|
+
return accounts.EXIT_ERROR
|
|
888
|
+
# 先确认账号可用、找得到 code 命令,再建目录:任何失败都不留下空的用户数据目录。
|
|
889
|
+
process_env, code = _usable_account_env(config, account)
|
|
890
|
+
if process_env is None:
|
|
891
|
+
return code
|
|
892
|
+
if shutil.which("code", path=process_env.get("PATH")) is None:
|
|
893
|
+
error("code not found in PATH{}".format(_CODE_NOT_FOUND_HINT))
|
|
894
|
+
return 127
|
|
895
|
+
data_dir = _private_dirs(expand(config.root), [".apps", account.dir, "vscode"])
|
|
896
|
+
if sys.platform == "darwin":
|
|
897
|
+
if len(data_dir) > _SOCKET_DIR_WARN_LENGTH:
|
|
898
|
+
warn("{} is long; VS Code's socket path inside it may exceed the 104-byte limit".format(data_dir))
|
|
899
|
+
warn("on macOS, code passes the whole environment (including this account's variables) to "
|
|
900
|
+
"`open --env`, so the values are briefly visible in the process list")
|
|
901
|
+
command = ["code", "--user-data-dir", data_dir] + ([path] if path else []) + list(code_args)
|
|
902
|
+
return _exec_as(config, account, command, not_found_hint=_CODE_NOT_FOUND_HINT)
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
def _private_dirs(root: str, parts: List[str]) -> str:
|
|
906
|
+
"""在 root 下逐级创建 parts,每一级都只允许本人访问(makedirs 的 mode 受 umask 影响,所以再 chmod)。"""
|
|
907
|
+
path = root
|
|
908
|
+
for part in parts:
|
|
909
|
+
path = os.path.join(path, part)
|
|
910
|
+
os.makedirs(path, mode=0o700, exist_ok=True)
|
|
911
|
+
os.chmod(path, 0o700)
|
|
912
|
+
return path
|
|
913
|
+
|
|
914
|
+
|
|
850
915
|
def cmd_path(name: str) -> int:
|
|
851
916
|
"""打印账号目录;目录不存在时照样打印,但以 1 退出,脚本里 `cd "$(multi-claude path X)"` 不会静默进错目录。"""
|
|
852
917
|
config, _ = load_config()
|
|
@@ -19,7 +19,7 @@ POSITIONAL_KINDS = {
|
|
|
19
19
|
"args": ["name"], "remove": ["name"], "usage": ["name"], "completion": ["shell"],
|
|
20
20
|
"route": ["dir", "name"], "which": ["dir"], "rename": ["name"], "mcp": ["name"],
|
|
21
21
|
"statusline": ["statusline", "file"], "handoff": ["name"], "login": ["name"], "set": ["name"],
|
|
22
|
-
"run": ["name"], "path": ["name"], "restore": ["name"],
|
|
22
|
+
"run": ["name"], "path": ["name"], "restore": ["name"], "code": ["name", "file"],
|
|
23
23
|
}
|
|
24
24
|
# 选项值是路径、应按文件补全的选项。
|
|
25
25
|
PATH_OPTIONS = ("-f", "--file", "--root", "--bin-dir", "--shared-dir")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-claude-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -286,6 +286,22 @@ multi-claude add client-b --config-from work # start client-b with a copy of wor
|
|
|
286
286
|
|
|
287
287
|
`--config-from` copies `settings.json` once; afterwards the two files are independent. A different existing `settings.json` in the new account, or a `settings.json` that the account shares, is a conflict and nothing is changed. Use [shared resources](#shared-resources) for settings that should stay the same in every account.
|
|
288
288
|
|
|
289
|
+
### Opening VS Code for an account (experimental)
|
|
290
|
+
|
|
291
|
+
```sh
|
|
292
|
+
multi-claude code work ~/projects/app # a separate VS Code window for work
|
|
293
|
+
multi-claude code personal . -- --disable-gpu # arguments after -- go to VS Code
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
The Claude Code extension uses the `CLAUDE_CONFIG_DIR` of the environment VS Code was started with, so a VS Code opened from the Dock always uses `~/.claude`. `code` starts VS Code with the account's environment (the same as `run`) and a separate user data directory, `<root>/.apps/<account directory>/vscode`. A separate user data directory is required: otherwise VS Code hands the request to the instance that is already running, with its own environment. Each account's instance starts with default VS Code settings; extensions are shared because they live in `~/.vscode/extensions`.
|
|
297
|
+
|
|
298
|
+
Notes:
|
|
299
|
+
|
|
300
|
+
- If an instance for the account is already running, the request goes to it; changes to the proxy or environment variables apply after that instance quits.
|
|
301
|
+
- Setting `CLAUDE_CONFIG_DIR` in the extension's `claudeCode.environmentVariables` inside that instance overrides the account.
|
|
302
|
+
- On macOS the `code` command passes the whole environment to `open --env`, so the values are briefly visible in the process list.
|
|
303
|
+
- Verified with VS Code 1.139.1 and Claude Code extension 2.1.286 on macOS; `code` prints a warning because it relies on undocumented behaviour. It needs the `code` command on `PATH` (in VS Code: "Shell Command: Install 'code' command in PATH").
|
|
304
|
+
|
|
289
305
|
### Diagnostics and completion
|
|
290
306
|
|
|
291
307
|
`multi-claude doctor` checks, without changing anything: the configuration and any unfinished migration, whether `claude` and the launcher directory are on `PATH`, whether each launcher is up to date and not hidden by another file of the same name, whether each account directory exists and is private (`0700`), the default account's link, broken shared links, whether a login exists, and variables such as `ANTHROPIC_API_KEY` that override the accounts' logins. Every problem comes with the command that fixes it. It exits with 1 when there is an error; `--json` prints the results for scripts.
|
|
@@ -389,6 +405,7 @@ No. It never reads, copies or deletes credentials; its only keychain call checks
|
|
|
389
405
|
| `multi-claude mcp NAME [ARG ...]` | Run `claude mcp ARG ...` as that account. |
|
|
390
406
|
| `multi-claude run [NAME] [-- COMMAND ...]` | Run a command (default: `claude` with the fixed arguments) with the environment of the account's launcher. Without NAME, the account `claude-auto` would use. |
|
|
391
407
|
| `multi-claude path NAME` | Print the account's directory. |
|
|
408
|
+
| `multi-claude code NAME [PATH] [-- ARGS ...]` | Open a separate VS Code instance with the account's environment (experimental). |
|
|
392
409
|
| `multi-claude handoff TARGET [--from NAME] [--session ID] [--force]` | Copy a session to another account and print the command that resumes it there. |
|
|
393
410
|
| `multi-claude statusline install\|uninstall FILE` | Wrap the statusLine command in a settings file so that `usage` gets fresh numbers, or restore it. |
|
|
394
411
|
| `multi-claude completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
@@ -464,7 +481,7 @@ If you move an account directory yourself, its launcher reports that the directo
|
|
|
464
481
|
./install.sh --uninstall
|
|
465
482
|
```
|
|
466
483
|
|
|
467
|
-
This removes the tool only. Your configuration, account directories, logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
484
|
+
This removes the tool only. Your configuration, account directories (including the VS Code data in `<root>/.apps` created by `code`), logins and `claude-<name>` launchers stay; the launchers keep working because they do not depend on multi-claude. Run `multi-claude statusline uninstall FILE` first if you wrapped a status line.
|
|
468
485
|
|
|
469
486
|
## Contributing
|
|
470
487
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""v0.6.0 `code NAME [PATH]`:按账号打开独立的 VS Code 实例(方案 docs/feature/feature-vscode-launch.md §8)。
|
|
2
|
+
|
|
3
|
+
用假 `code` 脚本记录参数与环境,不启动真实的 VS Code。"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
from helpers import CliTestCase
|
|
12
|
+
|
|
13
|
+
FAKE_CODE = """#!/bin/sh
|
|
14
|
+
printf '%s\\n' "$@" > "$FAKE_CLAUDE_OUT.code-args"
|
|
15
|
+
env > "$FAKE_CLAUDE_OUT.code-env"
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CodeTest(CliTestCase):
|
|
20
|
+
def setUp(self):
|
|
21
|
+
super().setUp()
|
|
22
|
+
self.code_path = self.write(os.path.join(self.fakebin, "code"), FAKE_CODE, 0o755)
|
|
23
|
+
self.ok("add", "work", "--proxy", "7901")
|
|
24
|
+
self.data_dir = os.path.join(self.root, ".apps", "work", "vscode")
|
|
25
|
+
|
|
26
|
+
def received(self):
|
|
27
|
+
with open(self.fake_out + ".code-args") as handle:
|
|
28
|
+
args = handle.read().split("\n")[:-1]
|
|
29
|
+
with open(self.fake_out + ".code-env") as handle:
|
|
30
|
+
env = dict(line.split("=", 1) for line in handle.read().splitlines() if "=" in line)
|
|
31
|
+
return args, env
|
|
32
|
+
|
|
33
|
+
def test_t1_opens_with_account_environment(self):
|
|
34
|
+
result = self.ok("code", "work")
|
|
35
|
+
self.assertIn("experimental", result.err)
|
|
36
|
+
args, env = self.received()
|
|
37
|
+
self.assertEqual(args, ["--user-data-dir", self.data_dir])
|
|
38
|
+
self.assertEqual(env["CLAUDE_CONFIG_DIR"], os.path.join(self.root, "work"))
|
|
39
|
+
self.assertEqual(env["HTTPS_PROXY"], "http://127.0.0.1:7901")
|
|
40
|
+
project = os.path.join(self.tmp, "proj")
|
|
41
|
+
self.ok("code", "work", project, "--", "--new-window")
|
|
42
|
+
args, _ = self.received()
|
|
43
|
+
self.assertEqual(args, ["--user-data-dir", self.data_dir, project, "--new-window"])
|
|
44
|
+
|
|
45
|
+
def test_t2_private_data_dir(self):
|
|
46
|
+
self.ok("code", "work")
|
|
47
|
+
self.ok("code", "work")
|
|
48
|
+
for path in (os.path.join(self.root, ".apps"), os.path.join(self.root, ".apps", "work"), self.data_dir):
|
|
49
|
+
self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o700, path)
|
|
50
|
+
# .apps 不是账号:list 与 apply 都不受影响
|
|
51
|
+
self.ok("apply")
|
|
52
|
+
self.assertEqual(self.ok("list", "--names").out, "work\n")
|
|
53
|
+
|
|
54
|
+
def test_t3_rename_keeps_data_dir(self):
|
|
55
|
+
self.ok("rename", "work", "client")
|
|
56
|
+
self.ok("code", "client")
|
|
57
|
+
args, _ = self.received()
|
|
58
|
+
self.assertEqual(args[:2], ["--user-data-dir", self.data_dir])
|
|
59
|
+
|
|
60
|
+
def test_t4_failures(self):
|
|
61
|
+
self.assertEqual(self.run_cli("code", "nosuch").code, 1)
|
|
62
|
+
os.unlink(self.code_path)
|
|
63
|
+
# PATH 只留假命令目录:系统目录里可能有真的 code(编译机 /usr/bin/code),不能让测试拉起真实的 VS Code
|
|
64
|
+
result = self.run_cli("code", "work", env={"PATH": self.fakebin})
|
|
65
|
+
self.assertEqual(result.code, 127, result)
|
|
66
|
+
self.assertIn("Install 'code' command in PATH", result.err)
|
|
67
|
+
self.assertFalse(os.path.lexists(os.path.join(self.root, ".apps")))
|
|
68
|
+
with open(os.path.join(self.state, "migrate-journal.json"), "w") as handle:
|
|
69
|
+
json.dump({"name": "main", "source": "s", "target": "t", "backup": "b", "mode": "rename",
|
|
70
|
+
"phase": "planned"}, handle)
|
|
71
|
+
result = self.run_cli("code", "work")
|
|
72
|
+
self.assertEqual(result.code, 1, result)
|
|
73
|
+
self.assertIn("unfinished migration", result.err)
|
|
74
|
+
|
|
75
|
+
def test_t4_more_failures_leave_no_data_dir(self):
|
|
76
|
+
self.assertEqual(self.run_cli("code").code, 2)
|
|
77
|
+
os.rmdir(os.path.join(self.root, "work"))
|
|
78
|
+
result = self.run_cli("code", "work")
|
|
79
|
+
self.assertEqual(result.code, 1, result)
|
|
80
|
+
self.assertFalse(os.path.lexists(os.path.join(self.root, ".apps")))
|
|
81
|
+
|
|
82
|
+
def test_t4_run_not_found_has_no_code_hint(self):
|
|
83
|
+
result = self.run_cli("run", "work", "--", "no-such-cmd-xyz")
|
|
84
|
+
self.assertEqual(result.code, 127)
|
|
85
|
+
self.assertNotIn("Install 'code'", result.err)
|
|
86
|
+
|
|
87
|
+
def test_t5_redirected_default_link(self):
|
|
88
|
+
source = os.path.join(self.home, ".claude")
|
|
89
|
+
self.write(os.path.join(source, ".credentials.json"), "{}", 0o600)
|
|
90
|
+
self.ok("migrate-default", "main")
|
|
91
|
+
os.unlink(source)
|
|
92
|
+
os.makedirs(source)
|
|
93
|
+
result = self.run_cli("code", "main")
|
|
94
|
+
self.assertEqual(result.code, 1, result)
|
|
95
|
+
self.assertFalse(os.path.lexists(os.path.join(self.root, ".apps", "main")))
|
|
96
|
+
|
|
97
|
+
def test_t5_remove_keeps_data_dir(self):
|
|
98
|
+
self.ok("code", "work")
|
|
99
|
+
self.ok("remove", "work")
|
|
100
|
+
self.assertTrue(os.path.isdir(self.data_dir))
|
|
101
|
+
|
|
102
|
+
@unittest.skipUnless(sys.platform == "darwin", "macOS-only warnings")
|
|
103
|
+
def test_t5_macos_warnings(self):
|
|
104
|
+
result = self.ok("code", "work")
|
|
105
|
+
self.assertIn("open --env", result.err)
|
|
106
|
+
# 提示只看用户数据目录的长度:macOS 的临时目录本身就很长,按实际长度判断应不应出现
|
|
107
|
+
if len(self.data_dir) > 80:
|
|
108
|
+
self.assertIn("is long", result.err)
|
|
109
|
+
else:
|
|
110
|
+
self.assertNotIn("is long", result.err)
|
|
111
|
+
|
|
112
|
+
def test_t5_default_identity_has_no_config_dir(self):
|
|
113
|
+
source = os.path.join(self.home, ".claude")
|
|
114
|
+
self.write(os.path.join(source, ".credentials.json"), "{}", 0o600)
|
|
115
|
+
self.ok("migrate-default", "main")
|
|
116
|
+
self.ok("code", "main")
|
|
117
|
+
_, env = self.received()
|
|
118
|
+
self.assertNotIn("CLAUDE_CONFIG_DIR", env)
|
|
119
|
+
|
|
120
|
+
def test_t6_help_and_completion(self):
|
|
121
|
+
self.assertIn("open VS Code for an account", self.ok("--help").out)
|
|
122
|
+
for shell in ("bash", "zsh", "fish"):
|
|
123
|
+
self.assertIn("code", self.ok("completion", shell).out)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
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
|
{multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{multi_claude_cli-0.5.0 → multi_claude_cli-0.6.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
|