multi-claude-cli 0.13.0__tar.gz → 0.15.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 (72) hide show
  1. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/PKG-INFO +55 -9
  2. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/README.md +53 -8
  3. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/pyproject.toml +1 -0
  4. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/__init__.py +1 -1
  5. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/__main__.py +7 -0
  6. multi_claude_cli-0.15.0/src/multi_claude/_entry.py +43 -0
  7. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/accounts.py +21 -1
  8. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/artifacts.py +4 -5
  9. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/bundle.py +11 -7
  10. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/cli.py +268 -17
  11. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/completion.py +97 -2
  12. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/config.py +19 -1
  13. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/doctor.py +11 -13
  14. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/env.py +24 -1
  15. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/fsutil.py +31 -6
  16. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/history.py +5 -1
  17. multi_claude_cli-0.15.0/src/multi_claude/hook.py +185 -0
  18. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/identity.py +27 -1
  19. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/launcher.py +127 -5
  20. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/lock.py +5 -6
  21. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/migrate.py +62 -8
  22. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/plan.py +4 -1
  23. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/platform.py +469 -15
  24. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/restore.py +24 -3
  25. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/routes.py +33 -5
  26. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/sessions.py +5 -5
  27. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/shared.py +27 -2
  28. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/shellpath.py +20 -2
  29. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/statusline.py +34 -5
  30. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/PKG-INFO +55 -9
  31. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/SOURCES.txt +4 -1
  32. multi_claude_cli-0.15.0/tests/test_help_examples.py +224 -0
  33. multi_claude_cli-0.15.0/tests/test_windows.py +502 -0
  34. multi_claude_cli-0.13.0/src/multi_claude/hook.py +0 -90
  35. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/LICENSE +0 -0
  36. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/setup.cfg +0 -0
  37. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/actions.py +0 -0
  38. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/refresh.py +0 -0
  39. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude/usage.py +0 -0
  40. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/dependency_links.txt +0 -0
  41. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/entry_points.txt +0 -0
  42. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/src/multi_claude_cli.egg-info/top_level.txt +0 -0
  43. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_accounts.py +0 -0
  44. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_artifacts.py +0 -0
  45. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_bundle.py +0 -0
  46. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_completion.py +0 -0
  47. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_config.py +0 -0
  48. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_doctor.py +0 -0
  49. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_everyday.py +0 -0
  50. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_fsutil.py +0 -0
  51. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_handoff.py +0 -0
  52. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_help.py +0 -0
  53. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_history.py +0 -0
  54. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_identity.py +0 -0
  55. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_install.py +0 -0
  56. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_mcp.py +0 -0
  57. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_migrate.py +0 -0
  58. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_onboarding.py +0 -0
  59. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_packaging.py +0 -0
  60. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_plan.py +0 -0
  61. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_platform.py +0 -0
  62. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_refresh.py +0 -0
  63. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_rename.py +0 -0
  64. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_renewal.py +0 -0
  65. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_routes.py +0 -0
  66. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_sessions.py +0 -0
  67. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_set.py +0 -0
  68. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_shared_exclude.py +0 -0
  69. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_statusline.py +0 -0
  70. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_usability.py +0 -0
  71. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.0}/tests/test_usage.py +0 -0
  72. {multi_claude_cli-0.13.0 → multi_claude_cli-0.15.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.13.0
3
+ Version: 0.15.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
@@ -11,6 +11,7 @@ Classifier: Environment :: Console
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Operating System :: MacOS
14
+ Classifier: Operating System :: Microsoft :: Windows
14
15
  Classifier: Operating System :: POSIX :: Linux
15
16
  Classifier: Programming Language :: Python :: 3
16
17
  Classifier: Programming Language :: Python :: 3.8
@@ -27,13 +28,14 @@ Dynamic: license-file
27
28
 
28
29
  # multi-claude
29
30
 
30
- **English** | [简体中文](README.zh-CN.md)
31
+ **English** | [简体中文](https://github.com/jakoes-wu/multi-claude/blob/main/README.zh-CN.md)
31
32
 
32
33
  [![Release](https://img.shields.io/github/v/release/jakoes-wu/multi-claude)](https://github.com/jakoes-wu/multi-claude/releases)
33
34
  [![CI](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml/badge.svg)](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml)
34
35
  [![PyPI](https://img.shields.io/pypi/v/multi-claude-cli)](https://pypi.org/project/multi-claude-cli/)
35
36
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
36
- [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
38
+ ![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)
37
39
 
38
40
  Use several [Claude Code](https://code.claude.com/docs) accounts on one machine, at the same time. Each account keeps its own login, settings and history and, if you like, its own proxy. No more logging out and in again.
39
41
 
@@ -84,7 +86,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
84
86
 
85
87
  | | Needed | Optional |
86
88
  | ---- | ---- | ---- |
87
- | Operating system | macOS or Linux (WSL counts as Linux) | — |
89
+ | Operating system | macOS, Linux (WSL counts as Linux) or Windows | — |
88
90
  | Tools | Python 3.8+ (`python3`, standard library only); Claude Code (`claude`); `curl` or `wget`, and `tar`, for the installer | `pipx` or Homebrew, to install from PyPI or the tap; `lsof`, so that `migrate-default` and `restore` can find processes using `~/.claude` (Linux falls back to `/proc`); VS Code's `code` command, for `multi-claude code` |
89
91
 
90
92
  Install what is missing:
@@ -100,7 +102,16 @@ sudo apt install -y python3 curl tar lsof
100
102
  curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
101
103
  ```
102
104
 
103
- Windows itself is not supported yet; use WSL. Other ways to install Claude Code: https://code.claude.com/docs/en/setup
105
+ On Windows, install Python 3.8+ from https://www.python.org/downloads/windows/ or the Microsoft Store, then in
106
+ PowerShell:
107
+
108
+ ```powershell
109
+ irm https://claude.ai/install.ps1 | iex # Claude Code, if you do not have it yet
110
+ python -m pip install --user pipx
111
+ python -m pipx ensurepath # then open a new terminal
112
+ ```
113
+
114
+ Other ways to install Claude Code: https://code.claude.com/docs/en/setup
104
115
 
105
116
  ## Installation
106
117
 
@@ -130,6 +141,38 @@ With Homebrew: `brew install jakoes-wu/tap/multi-claude` (upgrade with `brew upg
130
141
 
131
142
  Run `./install.sh --help` for all options.
132
143
 
144
+ ### Windows
145
+
146
+ Windows support is new. Install with `pipx install multi-claude-cli`; the installer script is for macOS and Linux
147
+ only. Everything above works the same way, with these differences:
148
+
149
+ - Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
150
+ `pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
151
+ and arguments from the configuration each time they start, so a change takes effect without `apply`.
152
+ - Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
153
+ - Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
154
+ (such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
155
+ developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
156
+ with `--shared-exclude` to share the rest.
157
+ - `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
158
+ another process uses. Close every Claude window, terminal and IDE first.
159
+ - `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
160
+ `statusLine` setting with `"shell": "powershell"` cannot be wrapped.
161
+ - Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
162
+ a conflict.
163
+ - cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
164
+ Python or package path has them (for example a Chinese user name), the launcher writes it as
165
+ `%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
166
+ that cannot be written with a variable and has no short name is reported as a conflict.
167
+ - When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
168
+ a batch file. It closes by itself; Claude has already exited and nothing is lost.
169
+ - `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
170
+ Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
171
+ exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
172
+
173
+ Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
174
+ suite runs on macOS and Linux only.
175
+
133
176
  ## Quick start
134
177
 
135
178
  ### 1. Create one account per login
@@ -456,6 +499,7 @@ Notes:
456
499
  multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
457
500
  multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
458
501
  multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
502
+ multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
459
503
  ```
460
504
 
461
505
  ## Migrating `~/.claude`
@@ -535,6 +579,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
535
579
 
536
580
  ### Commands
537
581
 
582
+ Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
583
+
538
584
  | Command | What it does |
539
585
  | ---- | ---- |
540
586
  | `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
@@ -575,13 +621,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
575
621
  | ---- | ---- |
576
622
  | Account directories | `~/.cc/<name>` |
577
623
  | Launchers | `~/.local/bin/claude-<name>` |
578
- | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
624
+ | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
579
625
 
580
626
  Account names may contain letters, digits and `._@+-`, must start with a letter or digit, and are case-insensitive (`Work` and `work` are the same account). An email address works as a name. Entries starting with `.` in the root directory are never treated as accounts.
581
627
 
582
628
  ### Declarative setup with `apply`
583
629
 
584
- See [`examples/config.example.json`](examples/config.example.json):
630
+ See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
585
631
 
586
632
  ```json
587
633
  {
@@ -642,7 +688,7 @@ This removes the tool only. Your configuration, account directories (including t
642
688
 
643
689
  ## Contributing
644
690
 
645
- See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
691
+ See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
646
692
 
647
693
  ```sh
648
694
  python3 -m unittest discover -s tests -t tests
@@ -650,4 +696,4 @@ python3 -m unittest discover -s tests -t tests
650
696
 
651
697
  ## License
652
698
 
653
- [MIT](LICENSE)
699
+ [MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
@@ -1,12 +1,13 @@
1
1
  # multi-claude
2
2
 
3
- **English** | [简体中文](README.zh-CN.md)
3
+ **English** | [简体中文](https://github.com/jakoes-wu/multi-claude/blob/main/README.zh-CN.md)
4
4
 
5
5
  [![Release](https://img.shields.io/github/v/release/jakoes-wu/multi-claude)](https://github.com/jakoes-wu/multi-claude/releases)
6
6
  [![CI](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml/badge.svg)](https://github.com/jakoes-wu/multi-claude/actions/workflows/ci.yml)
7
7
  [![PyPI](https://img.shields.io/pypi/v/multi-claude-cli)](https://pypi.org/project/multi-claude-cli/)
8
8
  ![Python](https://img.shields.io/badge/python-3.8%2B-blue)
9
- [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
10
+ ![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)
10
11
 
11
12
  Use several [Claude Code](https://code.claude.com/docs) accounts on one machine, at the same time. Each account keeps its own login, settings and history and, if you like, its own proxy. No more logging out and in again.
12
13
 
@@ -57,7 +58,7 @@ The launchers are plain shell scripts. They keep working even if you uninstall m
57
58
 
58
59
  | | Needed | Optional |
59
60
  | ---- | ---- | ---- |
60
- | Operating system | macOS or Linux (WSL counts as Linux) | — |
61
+ | Operating system | macOS, Linux (WSL counts as Linux) or Windows | — |
61
62
  | Tools | Python 3.8+ (`python3`, standard library only); Claude Code (`claude`); `curl` or `wget`, and `tar`, for the installer | `pipx` or Homebrew, to install from PyPI or the tap; `lsof`, so that `migrate-default` and `restore` can find processes using `~/.claude` (Linux falls back to `/proc`); VS Code's `code` command, for `multi-claude code` |
62
63
 
63
64
  Install what is missing:
@@ -73,7 +74,16 @@ sudo apt install -y python3 curl tar lsof
73
74
  curl -fsSL https://claude.ai/install.sh | bash # Claude Code, if you do not have it yet
74
75
  ```
75
76
 
76
- Windows itself is not supported yet; use WSL. Other ways to install Claude Code: https://code.claude.com/docs/en/setup
77
+ On Windows, install Python 3.8+ from https://www.python.org/downloads/windows/ or the Microsoft Store, then in
78
+ PowerShell:
79
+
80
+ ```powershell
81
+ irm https://claude.ai/install.ps1 | iex # Claude Code, if you do not have it yet
82
+ python -m pip install --user pipx
83
+ python -m pipx ensurepath # then open a new terminal
84
+ ```
85
+
86
+ Other ways to install Claude Code: https://code.claude.com/docs/en/setup
77
87
 
78
88
  ## Installation
79
89
 
@@ -103,6 +113,38 @@ With Homebrew: `brew install jakoes-wu/tap/multi-claude` (upgrade with `brew upg
103
113
 
104
114
  Run `./install.sh --help` for all options.
105
115
 
116
+ ### Windows
117
+
118
+ Windows support is new. Install with `pipx install multi-claude-cli`; the installer script is for macOS and Linux
119
+ only. Everything above works the same way, with these differences:
120
+
121
+ - Launchers are `claude-<name>.cmd` and `claude-auto.cmd` in `%USERPROFILE%\.local\bin` (the directory
122
+ `pipx ensurepath` adds to `PATH`), so they run from PowerShell and cmd alike. They read the proxy, variables
123
+ and arguments from the configuration each time they start, so a change takes effect without `apply`.
124
+ - Account directories are in `%USERPROFILE%\.cc` and the configuration in `%APPDATA%\multi-claude`.
125
+ - Shared directories are linked with directory junctions, which need no special rights. Sharing a single file
126
+ (such as `CLAUDE.md`) needs a file symbolic link, which needs Developer Mode (Settings > System > For
127
+ developers) or an administrator terminal; without it, `add` and `apply` stop and say so. Exclude the file
128
+ with `--shared-exclude` to share the rest.
129
+ - `migrate-default` refuses while any Claude process is running: Windows does not let it see which account
130
+ another process uses. Close every Claude window, terminal and IDE first.
131
+ - `statusline` needs Git for Windows, whose Git Bash is what Claude Code uses to run the status line. A
132
+ `statusLine` setting with `"shell": "powershell"` cannot be wrapped.
133
+ - Paths containing `"`, `%` or `!` cannot be written into a `.cmd` launcher; `apply` reports such an account as
134
+ a conflict.
135
+ - cmd reads a `.cmd` file in the console code page, so a launcher never contains non-ASCII characters. When the
136
+ Python or package path has them (for example a Chinese user name), the launcher writes it as
137
+ `%USERPROFILE%\...` (or `%LOCALAPPDATA%`, `%APPDATA%`), or else as its 8.3 short name. A non-ASCII path
138
+ that cannot be written with a variable and has no short name is reported as a conflict.
139
+ - When Claude exits (Ctrl+C or `/exit`), cmd may flash "Terminate batch job (Y/N)?" for a moment: the launcher is
140
+ a batch file. It closes by itself; Claude has already exited and nothing is lost.
141
+ - `multi-claude completion powershell | Add-Content $PROFILE` adds tab completion to PowerShell. Do not use `>>`:
142
+ Windows PowerShell 5.1 writes it as UTF-16 and breaks an existing profile. If the profile's directory does not
143
+ exist yet, create it first with `New-Item -ItemType Directory -Force (Split-Path $PROFILE)`.
144
+
145
+ Continuous integration runs the Windows-specific tests on Windows (`tests/test_windows.py`); the rest of the test
146
+ suite runs on macOS and Linux only.
147
+
106
148
  ## Quick start
107
149
 
108
150
  ### 1. Create one account per login
@@ -429,6 +471,7 @@ Notes:
429
471
  multi-claude completion bash > ~/.local/share/bash-completion/completions/multi-claude
430
472
  multi-claude completion zsh > "${fpath[1]}/_multi-claude" # or any directory on your fpath
431
473
  multi-claude completion fish > ~/.config/fish/completions/multi-claude.fish
474
+ multi-claude completion powershell | Add-Content $PROFILE # Windows PowerShell
432
475
  ```
433
476
 
434
477
  ## Migrating `~/.claude`
@@ -508,6 +551,8 @@ It never copies, exports, uploads or backs up a login. One command touches one:
508
551
 
509
552
  ### Commands
510
553
 
554
+ Every command's `--help` ends with examples you can copy, covering its options, e.g. `multi-claude route --help`.
555
+
511
556
  | Command | What it does |
512
557
  | ---- | ---- |
513
558
  | `multi-claude init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
@@ -548,13 +593,13 @@ Every write command accepts `--dry-run`, and prints only what changes; add `--ve
548
593
  | ---- | ---- |
549
594
  | Account directories | `~/.cc/<name>` |
550
595
  | Launchers | `~/.local/bin/claude-<name>` |
551
- | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`) |
596
+ | Configuration | `~/.config/multi-claude/config.json` (honours `XDG_CONFIG_HOME`); on Windows `%APPDATA%\multi-claude\config.json` |
552
597
 
553
598
  Account names may contain letters, digits and `._@+-`, must start with a letter or digit, and are case-insensitive (`Work` and `work` are the same account). An email address works as a name. Entries starting with `.` in the root directory are never treated as accounts.
554
599
 
555
600
  ### Declarative setup with `apply`
556
601
 
557
- See [`examples/config.example.json`](examples/config.example.json):
602
+ See [`examples/config.example.json`](https://github.com/jakoes-wu/multi-claude/blob/main/examples/config.example.json):
558
603
 
559
604
  ```json
560
605
  {
@@ -615,7 +660,7 @@ This removes the tool only. Your configuration, account directories (including t
615
660
 
616
661
  ## Contributing
617
662
 
618
- See [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with:
663
+ See [CONTRIBUTING.md](https://github.com/jakoes-wu/multi-claude/blob/main/CONTRIBUTING.md). Run the tests with:
619
664
 
620
665
  ```sh
621
666
  python3 -m unittest discover -s tests -t tests
@@ -623,4 +668,4 @@ python3 -m unittest discover -s tests -t tests
623
668
 
624
669
  ## License
625
670
 
626
- [MIT](LICENSE)
671
+ [MIT](https://github.com/jakoes-wu/multi-claude/blob/main/LICENSE)
@@ -16,6 +16,7 @@ classifiers = [
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Operating System :: MacOS",
19
+ "Operating System :: Microsoft :: Windows",
19
20
  "Operating System :: POSIX :: Linux",
20
21
  "Programming Language :: Python :: 3",
21
22
  "Programming Language :: Python :: 3.8",
@@ -1,3 +1,3 @@
1
1
  """multi-claude:管理多个 Claude Code 账号的配置目录、启动命令、代理与启动参数。"""
2
2
 
3
- __version__ = "0.13.0"
3
+ __version__ = "0.15.0"
@@ -7,6 +7,13 @@ if sys.argv[1:2] == ["statusline-hook"]:
7
7
  from .hook import main as hook_main
8
8
  sys.exit(hook_main(sys.argv[2:]))
9
9
 
10
+ from . import platform # noqa: E402
11
+
12
+ # 闸门放在导入 cli 之前:不支持的平台上 cli 依赖的模块可能根本加载不了,要先给出一句能看懂的报错
13
+ if not platform.is_supported():
14
+ sys.stderr.write("multi-claude: unsupported platform {}\n".format(sys.platform))
15
+ sys.exit(1)
16
+
10
17
  from .cli import main # noqa: E402
11
18
 
12
19
  sys.exit(main())
@@ -0,0 +1,43 @@
1
+ """Windows 启动命令(claude-<名称>.cmd、claude-auto.cmd)与状态栏后台进程的入口脚本。
2
+
3
+ 调用方式固定为 `"<python>" -I "<包目录>\\_entry.py" ...`(方案 feature-windows-support §5.1 D2):
4
+ - 用 `-I` 而不用 `-m`:`-m` 会把当前目录放进模块搜索路径,用户项目里的 json.py 之类文件会顶替标准库;
5
+ `-I` 也让脚本所在的包目录不进搜索路径,包里的 platform.py 等就不会顶替同名标准库模块。
6
+ - `-I` 同时忽略 PYTHONPATH,源码检出里运行时解释器找不到本包,所以这里把包的上一级目录追加到搜索路径末尾。
7
+
8
+ 本文件只能用标准库,且必须在导入本包之前完成搜索路径的设置。
9
+ """
10
+
11
+ import os
12
+ import runpy
13
+ import sys
14
+
15
+
16
+ def _same_dir(a: str, b: str) -> bool:
17
+ # 启动命令里的路径可能是 8.3 短名(MULTI_~1),而包从解释器原有的长路径加载:
18
+ # 只比 normcase 会把同一目录判成两个。realpath 在 Windows 上会展开短名与链接。
19
+ if os.path.normcase(os.path.realpath(a)) == os.path.normcase(os.path.realpath(b)):
20
+ return True
21
+ try:
22
+ return os.path.samefile(a, b)
23
+ except OSError:
24
+ return False
25
+
26
+
27
+ def _main() -> None:
28
+ package_dir = os.path.dirname(os.path.abspath(__file__))
29
+ parent = os.path.dirname(package_dir)
30
+ # 追加到末尾而不是插到最前:已安装的同名标准库模块仍优先于包目录之外的任何东西
31
+ if parent not in sys.path:
32
+ sys.path.append(parent)
33
+ import multi_claude
34
+ loaded = os.path.dirname(os.path.abspath(multi_claude.__file__))
35
+ if not _same_dir(loaded, package_dir):
36
+ # 解释器的 site-packages 里另装了一份 multi_claude:继续运行就是在用另一个版本
37
+ sys.stderr.write("multi-claude: loaded the package from {} instead of {}; reinstall multi-claude\n".format(
38
+ loaded, package_dir))
39
+ sys.exit(1)
40
+ runpy.run_module("multi_claude", run_name="__main__", alter_sys=True)
41
+
42
+
43
+ _main()
@@ -53,6 +53,9 @@ def default_dir() -> str:
53
53
 
54
54
 
55
55
  def render_launcher(config: Config, account: Account) -> str:
56
+ if platform.IS_WINDOWS:
57
+ python, entry = launcher.windows_command_paths()
58
+ return launcher.render_windows(account.name, account_dir(config, account.name), python, entry)
56
59
  return launcher.render(account.name, account.identity, account_dir(config, account.name), default_dir(),
57
60
  account.proxy, config.effective_env(account), config.effective_args(account))
58
61
 
@@ -169,6 +172,11 @@ def _plan_router(old: Config, new: Config) -> List[Action]:
169
172
  actions.append(Action(CONFLICT, "router", new_path, "claude-auto is the directory router; an account "
170
173
  "named 'auto' cannot coexist with routes"))
171
174
 
175
+ if platform.IS_WINDOWS:
176
+ unsafe = launcher.cmd_unsafe_reason([], launcher.windows_command_paths())
177
+ if unsafe is not None:
178
+ actions.append(Action(CONFLICT, "router", new_path, unsafe))
179
+ return actions
172
180
  content = routes.render_router(new)
173
181
  kind = entry_kind(new_path)
174
182
  if kind == KIND_MISSING:
@@ -230,12 +238,18 @@ def _make_private_dir(directory: str) -> None:
230
238
  if not os.path.isdir(parent):
231
239
  os.makedirs(parent, mode=0o700)
232
240
  os.mkdir(directory, 0o700)
233
- os.chmod(directory, 0o700)
241
+ # Windows 上 chmod 只能改只读属性,访问控制靠用户目录的 ACL
242
+ if not platform.IS_WINDOWS:
243
+ os.chmod(directory, 0o700)
234
244
 
235
245
 
236
246
  def _plan_launcher(bin_dir: str, config: Config, account: Account) -> List[Action]:
237
247
  name = account.name
238
248
  path = launcher.launcher_path(bin_dir, name)
249
+ if platform.IS_WINDOWS:
250
+ unsafe = launcher.cmd_unsafe_reason([account_dir(config, name)], launcher.windows_command_paths())
251
+ if unsafe is not None:
252
+ return [Action(CONFLICT, "launcher", path, unsafe)]
239
253
  content = render_launcher(config, account)
240
254
  kind = entry_kind(path)
241
255
  if kind == KIND_MISSING:
@@ -296,6 +310,12 @@ def execute(old: Config, new: Config, actions: List[Action], *, dry_run: bool, v
296
310
  return EXIT_CONFLICT
297
311
  if dry_run:
298
312
  return EXIT_OK
313
+ # Windows 上没有权限建文件链接时,在写配置之前就停下,免得配置已写、链接只建了一半(方案 C4)
314
+ file_links = shared.file_links_needed(actions) if platform.IS_WINDOWS else []
315
+ if file_links and not platform.can_create_file_link():
316
+ error(shared.file_link_message("the file links " + ", ".join(file_links),
317
+ sorted({os.path.basename(path) for path in file_links})), phase="shared-link")
318
+ return EXIT_ERROR
299
319
  config_action = actions[0]
300
320
  if config_action.changes:
301
321
  try:
@@ -10,7 +10,6 @@ assistant 记录,结果写一行带 `tool_result` 块的 user 记录,发布
10
10
  这段字节的 SHA-256),查询时据此把副本里交接前的记录归回来源账号(§5.1.3)。
11
11
  """
12
12
 
13
- import fcntl
14
13
  import hashlib
15
14
  import json
16
15
  import os
@@ -244,13 +243,13 @@ def record_handoff(handoff: "sessions.Plan", source_account_dir: str, target_acc
244
243
  data = (json.dumps(entry, ensure_ascii=False, sort_keys=True) + "\n").encode("utf-8")
245
244
  path = log_path()
246
245
  os.makedirs(os.path.dirname(path), exist_ok=True)
247
- fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT, 0o600)
246
+ fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT | getattr(os, "O_BINARY", 0), 0o600)
248
247
  try:
249
248
  # 阻塞等待:两个 handoff 同时追加时排队,不交错。锁的是本文件,不是工具的写锁(handoff 不拿写锁)
250
- fcntl.flock(fd, fcntl.LOCK_EX)
249
+ platform.lock_fd(fd, blocking=True)
251
250
  # 只写描述符读不了末字节,所以上面用 O_RDWR 打开
252
251
  size = os.fstat(fd).st_size
253
- if size and os.pread(fd, 1, size - 1) != b"\n":
252
+ if size and platform.read_at(fd, 1, size - 1) != b"\n":
254
253
  data = b"\n" + data
255
254
  view = memoryview(data)
256
255
  while view:
@@ -258,7 +257,7 @@ def record_handoff(handoff: "sessions.Plan", source_account_dir: str, target_acc
258
257
  view = view[written:]
259
258
  os.fsync(fd)
260
259
  finally:
261
- # 关闭即释放 flock
260
+ # 关闭即释放锁(flock 与 msvcrt 字节锁都如此)
262
261
  os.close(fd)
263
262
 
264
263
 
@@ -22,7 +22,7 @@ import time
22
22
  from datetime import datetime, timezone
23
23
  from typing import Dict, List, Optional, Tuple
24
24
 
25
- from . import __version__, accounts, usage
25
+ from . import __version__, accounts, platform, usage
26
26
  from .actions import error, info, warn
27
27
  from .config import Account, Config
28
28
  from .fsutil import KIND_DIR, KIND_FILE, KIND_LINK, KIND_MISSING, atomic_write, entry_kind
@@ -114,10 +114,12 @@ def _create_temp(parent: str):
114
114
  for _attempt in range(100):
115
115
  path = os.path.join(parent, ".multi-claude-export.{}.tar.gz".format(os.urandom(6).hex()))
116
116
  try:
117
- fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
117
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0), 0o600)
118
118
  except FileExistsError:
119
119
  continue
120
- os.fchmod(fd, 0o600) # 不受 umask 影响:包里可能有密钥,只给本人读
120
+ # 不受 umask 影响:包里可能有密钥,只给本人读。Windows 没有 fchmod,权限由用户目录的 ACL 保证
121
+ if hasattr(os, "fchmod"):
122
+ os.fchmod(fd, 0o600)
121
123
  return path, fd
122
124
  raise OSError("cannot create a temporary file in {}".format(parent))
123
125
 
@@ -132,7 +134,7 @@ def _publish(tmp_path: str, out_path: str) -> None:
132
134
  # 不支持硬链接的文件系统(如 exFAT):再检查一次后改名
133
135
  if os.path.lexists(out_path):
134
136
  raise OSError("{} appeared while exporting; nothing was written".format(out_path))
135
- os.replace(tmp_path, out_path)
137
+ platform.retry_on_sharing_violation(os.replace, tmp_path, out_path)
136
138
  return
137
139
  os.unlink(tmp_path)
138
140
 
@@ -350,7 +352,9 @@ def _check_item_types(members: List[tarfile.TarInfo]) -> None:
350
352
  def _check_member(member: tarfile.TarInfo) -> None:
351
353
  name = member.name
352
354
  parts = name.split("/")
353
- if name.startswith("/") or "\\" in name or "\x00" in name or any(part in ("", ".", "..") for part in parts):
355
+ # Windows 上成员名里的冒号会被解析成盘符(C:x)或 NTFS 备用数据流(a:b),一律拒绝;POSIX 上冒号是普通字符
356
+ if (name.startswith("/") or "\\" in name or "\x00" in name or (platform.IS_WINDOWS and ":" in name)
357
+ or any(part in ("", ".", "..") for part in parts)):
354
358
  raise BundleError("unsafe path in bundle: {!r}".format(name))
355
359
  if not (member.isfile() or member.isdir()):
356
360
  raise BundleError("unsupported entry in bundle (only files and directories): {!r}".format(name))
@@ -453,9 +457,9 @@ def _apply(plan: _Plan, directory: str, staging: str, state_path: str) -> None:
453
457
  target = os.path.join(directory, item)
454
458
  if action == "replace":
455
459
  backup = _backup_name(target, stamp)
456
- os.rename(target, backup)
460
+ platform.retry_on_sharing_violation(os.rename, target, backup)
457
461
  info("backup: {}".format(backup))
458
- os.replace(os.path.join(staging, ITEMS_DIR, item), target)
462
+ platform.retry_on_sharing_violation(os.replace, os.path.join(staging, ITEMS_DIR, item), target)
459
463
  if data is None:
460
464
  return
461
465
  warn("Claude sessions of this account that are running keep their own copy of {} and may overwrite "