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.
Files changed (56) hide show
  1. {multi_claude_cli-0.5.0/src/multi_claude_cli.egg-info → multi_claude_cli-0.6.0}/PKG-INFO +19 -2
  2. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/README.md +18 -1
  3. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/__init__.py +1 -1
  4. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/cli.py +73 -8
  5. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/completion.py +1 -1
  6. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0/src/multi_claude_cli.egg-info}/PKG-INFO +19 -2
  7. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/SOURCES.txt +2 -1
  8. multi_claude_cli-0.6.0/tests/test_vscode.py +127 -0
  9. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/LICENSE +0 -0
  10. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/pyproject.toml +0 -0
  11. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/setup.cfg +0 -0
  12. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/__main__.py +0 -0
  13. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/accounts.py +0 -0
  14. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/actions.py +0 -0
  15. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/config.py +0 -0
  16. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/doctor.py +0 -0
  17. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/env.py +0 -0
  18. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/fsutil.py +0 -0
  19. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/hook.py +0 -0
  20. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/identity.py +0 -0
  21. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/launcher.py +0 -0
  22. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/lock.py +0 -0
  23. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/migrate.py +0 -0
  24. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/platform.py +0 -0
  25. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/restore.py +0 -0
  26. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/routes.py +0 -0
  27. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/sessions.py +0 -0
  28. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/shared.py +0 -0
  29. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/shellpath.py +0 -0
  30. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/statusline.py +0 -0
  31. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude/usage.py +0 -0
  32. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
  33. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
  34. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
  35. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_accounts.py +0 -0
  36. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_completion.py +0 -0
  37. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_config.py +0 -0
  38. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_doctor.py +0 -0
  39. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_everyday.py +0 -0
  40. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_fsutil.py +0 -0
  41. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_handoff.py +0 -0
  42. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_help.py +0 -0
  43. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_identity.py +0 -0
  44. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_install.py +0 -0
  45. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_mcp.py +0 -0
  46. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_migrate.py +0 -0
  47. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_onboarding.py +0 -0
  48. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_packaging.py +0 -0
  49. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_platform.py +0 -0
  50. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_rename.py +0 -0
  51. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_routes.py +0 -0
  52. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_set.py +0 -0
  53. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_shared_exclude.py +0 -0
  54. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_statusline.py +0 -0
  55. {multi_claude_cli-0.5.0 → multi_claude_cli-0.6.0}/tests/test_usability.py +0 -0
  56. {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.5.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
 
@@ -1,3 +1,3 @@
1
1
  """multi-claude:管理多个 Claude Code 账号的配置目录、启动命令、代理与启动参数。"""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -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] == "run":
311
- # run 的命令从第一个 `--` 之后原样取出;没有 `--` 与 `--` 后为空都表示“没有命令”(运行 claude)。
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.5.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
 
@@ -50,4 +50,5 @@ tests/test_set.py
50
50
  tests/test_shared_exclude.py
51
51
  tests/test_statusline.py
52
52
  tests/test_usability.py
53
- tests/test_usage.py
53
+ tests/test_usage.py
54
+ tests/test_vscode.py
@@ -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()