claude-session-sync-cli 0.1.2__tar.gz → 0.2.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.
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/CHANGELOG.md +15 -1
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/PKG-INFO +22 -5
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/README.md +21 -4
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/README.zh-CN.md +21 -4
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/src/claude_session_sync/cli.py +219 -11
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/tests/conftest.py +2 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/tests/test_sync.py +133 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/.gitignore +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/CONTRIBUTING.md +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/LICENSE +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/SECURITY.md +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/pyproject.toml +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/src/claude_session_sync/__init__.py +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/src/claude_session_sync/__main__.py +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/tests/__init__.py +0 -0
- {claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/tests/fakes.py +0 -0
|
@@ -4,6 +4,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-11-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Account names in the output: the account the CLI is signed in to is recognized from `~/.claude.json`, every `--apply` remembers it in `~/.claude-desktop-sync/accounts.json`, and `--set-alias <id>=<name>` names any account. Emails are always masked.
|
|
11
|
+
- `--account` accepts an ID prefix (such as the 8 characters shown in the output) and an optional `/<org-id-prefix>`; ambiguous prefixes are rejected.
|
|
12
|
+
|
|
13
|
+
## [0.1.3] - 2026-11-11
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `--lang en|zh` to pick the output language for a single run; it overrides `CLAUDE_SESSION_SYNC_LANG` and the locale.
|
|
17
|
+
- The English `--help` now shows a one-line hint in Chinese on how to switch, and the Chinese `--help` shows the English equivalent.
|
|
18
|
+
|
|
7
19
|
## [0.1.2] - 2026-11-11
|
|
8
20
|
|
|
9
21
|
### Fixed
|
|
@@ -26,7 +38,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
26
38
|
- `--version` and `--help` with examples.
|
|
27
39
|
- English and Chinese output, chosen from the locale; override with `CLAUDE_SESSION_SYNC_LANG=en|zh`.
|
|
28
40
|
|
|
29
|
-
[Unreleased]: https://github.com/xushenjie/claude-session-sync/compare/v0.
|
|
41
|
+
[Unreleased]: https://github.com/xushenjie/claude-session-sync/compare/v0.2.0...HEAD
|
|
42
|
+
[0.2.0]: https://github.com/xushenjie/claude-session-sync/compare/v0.1.3...v0.2.0
|
|
43
|
+
[0.1.3]: https://github.com/xushenjie/claude-session-sync/compare/v0.1.2...v0.1.3
|
|
30
44
|
[0.1.2]: https://github.com/xushenjie/claude-session-sync/compare/v0.1.1...v0.1.2
|
|
31
45
|
[0.1.1]: https://github.com/xushenjie/claude-session-sync/compare/v0.1.0...v0.1.1
|
|
32
46
|
[0.1.0]: https://github.com/xushenjie/claude-session-sync/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: claude-session-sync-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Sync local Claude Code CLI sessions into the Claude desktop app sidebar (unofficial).
|
|
5
5
|
Project-URL: Homepage, https://github.com/xushenjie/claude-session-sync
|
|
6
6
|
Project-URL: Issues, https://github.com/xushenjie/claude-session-sync/issues
|
|
@@ -126,7 +126,7 @@ claude-session-sync --undo
|
|
|
126
126
|
|
|
127
127
|
> If it says the desktop app is running, the app is still alive in the menu bar or system tray and **nothing was written**. Quit it fully and run again.
|
|
128
128
|
|
|
129
|
-
Output is in English, or in Chinese when your locale (`LC_ALL` / `LC_MESSAGES` / `LANG`, or the Windows display language) is Chinese.
|
|
129
|
+
Output is in English, or in Chinese when your locale (`LC_ALL` / `LC_MESSAGES` / `LANG`, or the Windows display language) is Chinese. Pass `--lang en` or `--lang zh` for a single run, or set `CLAUDE_SESSION_SYNC_LANG=en` or `zh` in your shell profile to change it for good without touching your locale.
|
|
130
130
|
|
|
131
131
|
---
|
|
132
132
|
|
|
@@ -178,11 +178,13 @@ claude-session-sync --purge-archived --apply # do it
|
|
|
178
178
|
|---|---|
|
|
179
179
|
| `-h`, `--help` | Show help, examples, and caveats |
|
|
180
180
|
| `-V`, `--version` | Show the version |
|
|
181
|
+
| `--lang en\|zh` | Output language; overrides `CLAUDE_SESSION_SYNC_LANG` and the locale |
|
|
181
182
|
| (no options) | Preview mode |
|
|
182
183
|
| `--apply` | Actually write |
|
|
183
184
|
| `--undo` | Undo the most recent write or deletion |
|
|
184
185
|
| `--show-template` | Print each account's template fields and exit (for troubleshooting) |
|
|
185
|
-
| `--account <account-id>` | Only sync this account;
|
|
186
|
+
| `--account <account-id>` | Only sync this account. A prefix such as the 8 characters in the output is enough; add `/<org-id-prefix>` to pick one organization. Repeatable |
|
|
187
|
+
| `--set-alias <id>=<name>` | Give an account a display name (see [Account names](https://github.com/xushenjie/claude-session-sync#account-names)); an empty name removes it. Repeatable; exits after saving |
|
|
186
188
|
| `--project <string>` | Only handle sessions whose working directory contains this string (sync and purge) |
|
|
187
189
|
| `--exclude <session-id>` | Exclude a session from sync and purge; repeatable |
|
|
188
190
|
| `--exclude-file <file>` | Read excluded session IDs from a file, one per line |
|
|
@@ -203,7 +205,22 @@ claude-session-sync --account 1a2b3c4d-... --project myapp --apply
|
|
|
203
205
|
claude-session-sync --exclude-file ~/no-sync.txt --apply
|
|
204
206
|
```
|
|
205
207
|
|
|
206
|
-
A session ID is the file name of a `.jsonl` under `~/.claude/projects/<project>/`. An account ID is a top-level directory name under `claude-code-sessions/`; the preview shows the first 8 characters.
|
|
208
|
+
A session ID is the file name of a `.jsonl` under `~/.claude/projects/<project>/`. An account ID is a top-level directory name under `claude-code-sessions/`; the preview shows the first 8 characters, which `--account` and `--set-alias` accept as-is.
|
|
209
|
+
|
|
210
|
+
### Account names
|
|
211
|
+
|
|
212
|
+
The desktop app stores accounts by ID only, so the output shows names where it can find them:
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
[work · Acme Inc · fan***@example.com · current CLI login] (b8073c71…/677f8bdf…) existing 122, to add 1
|
|
216
|
+
[07ac42e2…/00000000…] existing 119, to add 1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- **The account the CLI is signed in to** is recognized from `~/.claude.json` (only the account and organization IDs, email and organization name are read).
|
|
220
|
+
- **Accounts the CLI was signed in to before**: every `--apply` remembers the current CLI account in `~/.claude-desktop-sync/accounts.json`, so it keeps its name after you switch.
|
|
221
|
+
- **Any account**, with an alias you choose: `claude-session-sync --set-alias b8073c71=work`. Use `<account>/<org>` prefixes to name a single organization.
|
|
222
|
+
|
|
223
|
+
Emails are always shown masked (`fan***@example.com`), because this output often ends up in bug reports. Previews never write `accounts.json`.
|
|
207
224
|
|
|
208
225
|
---
|
|
209
226
|
|
|
@@ -233,7 +250,7 @@ Use the package name `claude-session-sync-cli` (not the command name) with your
|
|
|
233
250
|
Not sure which one you used? `uv tool list` or `pipx list` will show it.
|
|
234
251
|
|
|
235
252
|
- **Upgrading** replaces the old version in place; the command name stays the same. Running `uv tool install` again does not upgrade — use `upgrade`. Journals and backups in `~/.claude-desktop-sync/` are kept, so `--undo` still works across upgrades. Run `claude-session-sync --version` to confirm.
|
|
236
|
-
- **Uninstalling removes only the program.** Sessions it added to the desktop sidebar and everything in `~/.claude-desktop-sync/` stay. To revert those too, quit the desktop app and run `claude-session-sync --undo` repeatedly *before* uninstalling, until it reports nothing left to undo; then delete `~/.claude-desktop-sync/` if you no longer need the backups.
|
|
253
|
+
- **Uninstalling removes only the program.** Sessions it added to the desktop sidebar and everything in `~/.claude-desktop-sync/` stay. To revert those too, quit the desktop app and run `claude-session-sync --undo` repeatedly *before* uninstalling, until it reports nothing left to undo; then delete `~/.claude-desktop-sync/` if you no longer need the backups and saved account names.
|
|
237
254
|
|
|
238
255
|
---
|
|
239
256
|
|
|
@@ -97,7 +97,7 @@ claude-session-sync --undo
|
|
|
97
97
|
|
|
98
98
|
> If it says the desktop app is running, the app is still alive in the menu bar or system tray and **nothing was written**. Quit it fully and run again.
|
|
99
99
|
|
|
100
|
-
Output is in English, or in Chinese when your locale (`LC_ALL` / `LC_MESSAGES` / `LANG`, or the Windows display language) is Chinese.
|
|
100
|
+
Output is in English, or in Chinese when your locale (`LC_ALL` / `LC_MESSAGES` / `LANG`, or the Windows display language) is Chinese. Pass `--lang en` or `--lang zh` for a single run, or set `CLAUDE_SESSION_SYNC_LANG=en` or `zh` in your shell profile to change it for good without touching your locale.
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
@@ -149,11 +149,13 @@ claude-session-sync --purge-archived --apply # do it
|
|
|
149
149
|
|---|---|
|
|
150
150
|
| `-h`, `--help` | Show help, examples, and caveats |
|
|
151
151
|
| `-V`, `--version` | Show the version |
|
|
152
|
+
| `--lang en\|zh` | Output language; overrides `CLAUDE_SESSION_SYNC_LANG` and the locale |
|
|
152
153
|
| (no options) | Preview mode |
|
|
153
154
|
| `--apply` | Actually write |
|
|
154
155
|
| `--undo` | Undo the most recent write or deletion |
|
|
155
156
|
| `--show-template` | Print each account's template fields and exit (for troubleshooting) |
|
|
156
|
-
| `--account <account-id>` | Only sync this account;
|
|
157
|
+
| `--account <account-id>` | Only sync this account. A prefix such as the 8 characters in the output is enough; add `/<org-id-prefix>` to pick one organization. Repeatable |
|
|
158
|
+
| `--set-alias <id>=<name>` | Give an account a display name (see [Account names](https://github.com/xushenjie/claude-session-sync#account-names)); an empty name removes it. Repeatable; exits after saving |
|
|
157
159
|
| `--project <string>` | Only handle sessions whose working directory contains this string (sync and purge) |
|
|
158
160
|
| `--exclude <session-id>` | Exclude a session from sync and purge; repeatable |
|
|
159
161
|
| `--exclude-file <file>` | Read excluded session IDs from a file, one per line |
|
|
@@ -174,7 +176,22 @@ claude-session-sync --account 1a2b3c4d-... --project myapp --apply
|
|
|
174
176
|
claude-session-sync --exclude-file ~/no-sync.txt --apply
|
|
175
177
|
```
|
|
176
178
|
|
|
177
|
-
A session ID is the file name of a `.jsonl` under `~/.claude/projects/<project>/`. An account ID is a top-level directory name under `claude-code-sessions/`; the preview shows the first 8 characters.
|
|
179
|
+
A session ID is the file name of a `.jsonl` under `~/.claude/projects/<project>/`. An account ID is a top-level directory name under `claude-code-sessions/`; the preview shows the first 8 characters, which `--account` and `--set-alias` accept as-is.
|
|
180
|
+
|
|
181
|
+
### Account names
|
|
182
|
+
|
|
183
|
+
The desktop app stores accounts by ID only, so the output shows names where it can find them:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
[work · Acme Inc · fan***@example.com · current CLI login] (b8073c71…/677f8bdf…) existing 122, to add 1
|
|
187
|
+
[07ac42e2…/00000000…] existing 119, to add 1
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **The account the CLI is signed in to** is recognized from `~/.claude.json` (only the account and organization IDs, email and organization name are read).
|
|
191
|
+
- **Accounts the CLI was signed in to before**: every `--apply` remembers the current CLI account in `~/.claude-desktop-sync/accounts.json`, so it keeps its name after you switch.
|
|
192
|
+
- **Any account**, with an alias you choose: `claude-session-sync --set-alias b8073c71=work`. Use `<account>/<org>` prefixes to name a single organization.
|
|
193
|
+
|
|
194
|
+
Emails are always shown masked (`fan***@example.com`), because this output often ends up in bug reports. Previews never write `accounts.json`.
|
|
178
195
|
|
|
179
196
|
---
|
|
180
197
|
|
|
@@ -204,7 +221,7 @@ Use the package name `claude-session-sync-cli` (not the command name) with your
|
|
|
204
221
|
Not sure which one you used? `uv tool list` or `pipx list` will show it.
|
|
205
222
|
|
|
206
223
|
- **Upgrading** replaces the old version in place; the command name stays the same. Running `uv tool install` again does not upgrade — use `upgrade`. Journals and backups in `~/.claude-desktop-sync/` are kept, so `--undo` still works across upgrades. Run `claude-session-sync --version` to confirm.
|
|
207
|
-
- **Uninstalling removes only the program.** Sessions it added to the desktop sidebar and everything in `~/.claude-desktop-sync/` stay. To revert those too, quit the desktop app and run `claude-session-sync --undo` repeatedly *before* uninstalling, until it reports nothing left to undo; then delete `~/.claude-desktop-sync/` if you no longer need the backups.
|
|
224
|
+
- **Uninstalling removes only the program.** Sessions it added to the desktop sidebar and everything in `~/.claude-desktop-sync/` stay. To revert those too, quit the desktop app and run `claude-session-sync --undo` repeatedly *before* uninstalling, until it reports nothing left to undo; then delete `~/.claude-desktop-sync/` if you no longer need the backups and saved account names.
|
|
208
225
|
|
|
209
226
|
---
|
|
210
227
|
|
|
@@ -97,7 +97,7 @@ claude-session-sync --undo
|
|
|
97
97
|
|
|
98
98
|
> 如果提示"检测到桌面端正在运行",说明桌面端还挂在菜单栏或托盘里,**这次什么都没有写入**。彻底退出后重新运行即可。
|
|
99
99
|
|
|
100
|
-
系统语言(`LC_ALL`、`LC_MESSAGES`、`LANG`,或 Windows
|
|
100
|
+
系统语言(`LC_ALL`、`LC_MESSAGES`、`LANG`,或 Windows 的显示语言)是中文时输出中文,否则输出英文。临时切换加 `--lang zh` 或 `--lang en`;想长期使用中文,又不想改系统语言,在 shell 配置文件(如 `~/.zshrc`)里加上 `export CLAUDE_SESSION_SYNC_LANG=zh`。
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
@@ -149,11 +149,13 @@ claude-session-sync --purge-archived --apply # 执行
|
|
|
149
149
|
|---|---|
|
|
150
150
|
| `-h`, `--help` | 显示帮助、示例和注意事项 |
|
|
151
151
|
| `-V`, `--version` | 显示版本号 |
|
|
152
|
+
| `--lang en\|zh` | 输出语言,优先于 `CLAUDE_SESSION_SYNC_LANG` 和系统语言 |
|
|
152
153
|
| (不加参数) | 预览模式 |
|
|
153
154
|
| `--apply` | 实际写入 |
|
|
154
155
|
| `--undo` | 撤销最近一次写入或删除 |
|
|
155
156
|
| `--show-template` | 显示每个账号的模板字段后退出(排查用) |
|
|
156
|
-
| `--account <账号ID>` |
|
|
157
|
+
| `--account <账号ID>` | 只同步指定账号。写输出里显示的前 8 位就够了;加 `/<组织ID前缀>` 可以只选一个组织。可以写多次 |
|
|
158
|
+
| `--set-alias <ID>=<名称>` | 给账号起显示名称(见[账号名称](#账号名称)),名称留空则删除。可以写多次,保存后退出 |
|
|
157
159
|
| `--project <字符串>` | 只处理工作目录里包含这个字符串的会话(同步和清除都生效) |
|
|
158
160
|
| `--exclude <会话ID>` | 排除某个会话,同步和清除都跳过,可以写多次 |
|
|
159
161
|
| `--exclude-file <文件>` | 从文件读取排除列表,每行一个会话 ID |
|
|
@@ -174,7 +176,22 @@ claude-session-sync --account 1a2b3c4d-... --project myapp --apply
|
|
|
174
176
|
claude-session-sync --exclude-file ~/no-sync.txt --apply
|
|
175
177
|
```
|
|
176
178
|
|
|
177
|
-
会话 ID 就是 `~/.claude/projects/<项目>/` 下 `.jsonl` 文件的文件名;账号 ID 是 `claude-code-sessions/` 下第一层目录名,预览输出里会显示前 8
|
|
179
|
+
会话 ID 就是 `~/.claude/projects/<项目>/` 下 `.jsonl` 文件的文件名;账号 ID 是 `claude-code-sessions/` 下第一层目录名,预览输出里会显示前 8 位,`--account` 和 `--set-alias` 直接用这 8 位就行。
|
|
180
|
+
|
|
181
|
+
### 账号名称
|
|
182
|
+
|
|
183
|
+
桌面端只按 ID 存放账号,所以输出里会尽量把能找到的名称显示出来:
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
[工作号 · Acme Inc · fan***@example.com · 当前 CLI 登录] (b8073c71…/677f8bdf…) 已有 122,待补 1
|
|
187
|
+
[07ac42e2…/00000000…] 已有 119,待补 1
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **CLI 当前登录的账号**:从 `~/.claude.json` 自动识别(只读取账号和组织 ID、邮箱、组织名)。
|
|
191
|
+
- **CLI 以前登录过的账号**:每次 `--apply` 都会把当前 CLI 账号记到 `~/.claude-desktop-sync/accounts.json`,切换账号后仍然能显示名称。
|
|
192
|
+
- **任意账号**:自己起别名,比如 `claude-session-sync --set-alias b8073c71=工作号`;写成 `<账号>/<组织>` 前缀可以只给某个组织起名。
|
|
193
|
+
|
|
194
|
+
邮箱始终打码显示(如 `fan***@example.com`),因为这份输出经常会被贴进 issue。预览模式不会写入 `accounts.json`。
|
|
178
195
|
|
|
179
196
|
---
|
|
180
197
|
|
|
@@ -204,7 +221,7 @@ claude-session-sync --exclude-file ~/no-sync.txt --apply
|
|
|
204
221
|
不记得用哪种方式装的,运行 `uv tool list` 或 `pipx list` 看一下。
|
|
205
222
|
|
|
206
223
|
- **升级**会直接替换旧版本,命令名不变。再运行一次 `uv tool install` 不会升级,要用 `upgrade`。`~/.claude-desktop-sync/` 里的日志和备份会保留,升级后 `--undo` 依然可以撤销以前的操作。升级完可以用 `claude-session-sync --version` 确认版本。
|
|
207
|
-
- **卸载只删除程序本身。** 已经同步到桌面端侧边栏的会话和 `~/.claude-desktop-sync/` 都会保留。想一并撤销,请在**卸载前**退出桌面端,反复运行 `claude-session-sync --undo`,直到提示没有可撤销的记录;不再需要备份的话,再手动删除 `~/.claude-desktop-sync
|
|
224
|
+
- **卸载只删除程序本身。** 已经同步到桌面端侧边栏的会话和 `~/.claude-desktop-sync/` 都会保留。想一并撤销,请在**卸载前**退出桌面端,反复运行 `claude-session-sync --undo`,直到提示没有可撤销的记录;不再需要备份的话,再手动删除 `~/.claude-desktop-sync/`(里面也存着账号名称和别名)。
|
|
208
225
|
|
|
209
226
|
---
|
|
210
227
|
|
{claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/src/claude_session_sync/cli.py
RENAMED
|
@@ -33,6 +33,7 @@ import glob
|
|
|
33
33
|
import json
|
|
34
34
|
import os
|
|
35
35
|
import platform
|
|
36
|
+
import re
|
|
36
37
|
import shutil
|
|
37
38
|
import subprocess
|
|
38
39
|
import sys
|
|
@@ -40,7 +41,7 @@ import time
|
|
|
40
41
|
import uuid
|
|
41
42
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
42
43
|
|
|
43
|
-
__version__ = "0.
|
|
44
|
+
__version__ = "0.2.0"
|
|
44
45
|
|
|
45
46
|
HOME = Path.home()
|
|
46
47
|
JOURNAL_DIR = HOME / ".claude-desktop-sync"
|
|
@@ -578,20 +579,189 @@ def purge_archived(indexes, archived_ids, projects, apply, trash, excluded=(), p
|
|
|
578
579
|
return created, moved
|
|
579
580
|
|
|
580
581
|
|
|
582
|
+
# ---------------------------------------------------------------- 账号名称
|
|
583
|
+
#
|
|
584
|
+
# 桌面端只按 ID 存放账号目录,本机没有公开的地方记录其他账号的邮箱或组织名。
|
|
585
|
+
# 名称来源(按优先级):
|
|
586
|
+
# 1. 用户用 --set-alias 起的别名
|
|
587
|
+
# 2. CLI 当前登录的账号(~/.claude.json 的 oauthAccount)
|
|
588
|
+
# 3. 以前 --apply 时记下的 CLI 账号信息(~/.claude-desktop-sync/accounts.json)
|
|
589
|
+
# 邮箱一律打码后再显示,因为预览输出经常会被贴进 issue。
|
|
590
|
+
|
|
591
|
+
EMAIL_RE = re.compile(r"([A-Za-z0-9._%+-]+)@([A-Za-z0-9.-]+\.[A-Za-z]{2,})")
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def mask_email(email):
|
|
595
|
+
local, _, domain = email.partition("@")
|
|
596
|
+
keep = min(3, max(1, len(local) // 2))
|
|
597
|
+
return f"{local[:keep]}***@{domain}"
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def mask_emails_in(text):
|
|
601
|
+
return EMAIL_RE.sub(lambda m: mask_email(m.group(0)), text)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def accounts_file():
|
|
605
|
+
return JOURNAL_DIR / "accounts.json"
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def load_account_book():
|
|
609
|
+
try:
|
|
610
|
+
data = json.loads(accounts_file().read_text(encoding="utf-8"))
|
|
611
|
+
except (OSError, ValueError):
|
|
612
|
+
data = {}
|
|
613
|
+
if not isinstance(data, dict):
|
|
614
|
+
data = {}
|
|
615
|
+
return {
|
|
616
|
+
"accounts": data.get("accounts") if isinstance(data.get("accounts"), dict) else {},
|
|
617
|
+
"aliases": data.get("aliases") if isinstance(data.get("aliases"), dict) else {},
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def save_account_book(book):
|
|
622
|
+
JOURNAL_DIR.mkdir(parents=True, exist_ok=True)
|
|
623
|
+
p = accounts_file()
|
|
624
|
+
p.write_text(json.dumps({"version": 1, **book}, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
625
|
+
try:
|
|
626
|
+
os.chmod(p, 0o600) # 里面有邮箱,只让自己读
|
|
627
|
+
except OSError:
|
|
628
|
+
pass
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def cli_login_account():
|
|
632
|
+
"""读取 CLI 当前登录账号,只取识别账号需要的字段。读不到返回 None。"""
|
|
633
|
+
cfg = os.environ.get("CLAUDE_CONFIG_DIR")
|
|
634
|
+
candidates = ([Path(cfg).expanduser() / ".claude.json"] if cfg else []) + [HOME / ".claude.json"]
|
|
635
|
+
for f in candidates:
|
|
636
|
+
try:
|
|
637
|
+
a = json.loads(f.read_text(encoding="utf-8")).get("oauthAccount")
|
|
638
|
+
except (OSError, ValueError, AttributeError):
|
|
639
|
+
continue
|
|
640
|
+
if isinstance(a, dict) and isinstance(a.get("accountUuid"), str):
|
|
641
|
+
return {
|
|
642
|
+
"account": a["accountUuid"],
|
|
643
|
+
"org": a.get("organizationUuid") or "",
|
|
644
|
+
"email": a.get("emailAddress") or "",
|
|
645
|
+
"org_name": a.get("organizationName") or "",
|
|
646
|
+
}
|
|
647
|
+
return None
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def remember_cli_account(book, cli_acct):
|
|
651
|
+
"""把 CLI 当前账号记进账号簿;有变化返回 True。"""
|
|
652
|
+
if not cli_acct:
|
|
653
|
+
return False
|
|
654
|
+
key = f"{cli_acct['account']}/{cli_acct['org']}"
|
|
655
|
+
info = {"email": cli_acct["email"], "org_name": cli_acct["org_name"]}
|
|
656
|
+
old = book["accounts"].get(key) or {}
|
|
657
|
+
if {k: old.get(k) for k in info} == info:
|
|
658
|
+
return False
|
|
659
|
+
book["accounts"][key] = {**info, "seen_at": dt.datetime.now().isoformat(timespec="seconds")}
|
|
660
|
+
return True
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def account_label(acct, org, book, cli_acct):
|
|
664
|
+
"""返回账号的可读名称(可能为空)。"""
|
|
665
|
+
parts = []
|
|
666
|
+
alias = book["aliases"].get(f"{acct}/{org}") or book["aliases"].get(acct)
|
|
667
|
+
if alias:
|
|
668
|
+
parts.append(alias)
|
|
669
|
+
is_cli = bool(cli_acct and cli_acct["account"] == acct and cli_acct["org"] == org)
|
|
670
|
+
info = (
|
|
671
|
+
{"email": cli_acct["email"], "org_name": cli_acct["org_name"]}
|
|
672
|
+
if is_cli
|
|
673
|
+
else book["accounts"].get(f"{acct}/{org}") or {}
|
|
674
|
+
)
|
|
675
|
+
email = mask_email(info["email"]) if EMAIL_RE.fullmatch(info.get("email") or "") else ""
|
|
676
|
+
org_name = mask_emails_in(info.get("org_name") or "")
|
|
677
|
+
if org_name and not (email and email in org_name): # 个人组织名常常就是"邮箱's Organization"
|
|
678
|
+
parts.append(org_name)
|
|
679
|
+
if email:
|
|
680
|
+
parts.append(email)
|
|
681
|
+
if is_cli:
|
|
682
|
+
parts.append(t("current CLI login", "当前 CLI 登录"))
|
|
683
|
+
return " · ".join(parts)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def account_heading(acct, org, book, cli_acct):
|
|
687
|
+
ids = f"{acct[:8]}…/{org[:8]}…"
|
|
688
|
+
label = account_label(acct, org, book, cli_acct)
|
|
689
|
+
return f"[{label}] ({ids})" if label else f"[{ids}]"
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
def resolve_account_key(key, all_accts):
|
|
693
|
+
"""把 <账号ID前缀>[/<组织ID前缀>] 解析成完整 ID。返回 (账号ID, 组织ID或None);出错时退出。"""
|
|
694
|
+
acct_part, _, org_part = key.partition("/")
|
|
695
|
+
if not acct_part:
|
|
696
|
+
sys.exit(t(f"Invalid account ID: {key!r}", f"账号 ID 无效:{key!r}"))
|
|
697
|
+
pairs = {(a, o) for _, a, o, _ in all_accts if a.startswith(acct_part) and o.startswith(org_part)}
|
|
698
|
+
acct_ids = {a for a, _ in pairs}
|
|
699
|
+
if not pairs:
|
|
700
|
+
sys.exit(t(f"No account matches {key!r}.", f"没有匹配 {key!r} 的账号。"))
|
|
701
|
+
if len(acct_ids) > 1:
|
|
702
|
+
sys.exit(
|
|
703
|
+
t(
|
|
704
|
+
f"{key!r} matches several accounts: {', '.join(sorted(x[:8] for x in acct_ids))}. Use a longer prefix.",
|
|
705
|
+
f"{key!r} 匹配到多个账号:{', '.join(sorted(x[:8] for x in acct_ids))}。请写更长的前缀。",
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
acct = acct_ids.pop()
|
|
709
|
+
if not org_part:
|
|
710
|
+
return acct, None
|
|
711
|
+
if len(pairs) > 1:
|
|
712
|
+
sys.exit(t(f"{key!r} matches several organizations.", f"{key!r} 匹配到多个组织。"))
|
|
713
|
+
return acct, next(iter(pairs))[1]
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def set_aliases(specs, all_accts):
|
|
717
|
+
book = load_account_book()
|
|
718
|
+
for spec in specs:
|
|
719
|
+
key, sep, name = spec.partition("=")
|
|
720
|
+
if not sep:
|
|
721
|
+
sys.exit(
|
|
722
|
+
t(f"--set-alias expects ID=NAME, got {spec!r}", f"--set-alias 的格式是 ID=名称,收到的是 {spec!r}")
|
|
723
|
+
)
|
|
724
|
+
acct, org = resolve_account_key(key.strip(), all_accts)
|
|
725
|
+
full = f"{acct}/{org}" if org else acct
|
|
726
|
+
name = name.strip()
|
|
727
|
+
if name:
|
|
728
|
+
book["aliases"][full] = name
|
|
729
|
+
print(t(f"Alias set: {full} = {name}", f"已设置别名:{full} = {name}"))
|
|
730
|
+
else:
|
|
731
|
+
book["aliases"].pop(full, None)
|
|
732
|
+
print(t(f"Alias removed: {full}", f"已删除别名:{full}"))
|
|
733
|
+
save_account_book(book)
|
|
734
|
+
|
|
735
|
+
|
|
581
736
|
# ---------------------------------------------------------------- 主流程
|
|
582
737
|
|
|
583
738
|
|
|
739
|
+
def apply_lang_flag(argv):
|
|
740
|
+
"""在构建帮助文本之前先读取 --lang,让 --help 和后续输出都使用指定语言。
|
|
741
|
+
非法取值交给正式的解析器报错。"""
|
|
742
|
+
global _LANG_ZH
|
|
743
|
+
pre = argparse.ArgumentParser(add_help=False)
|
|
744
|
+
pre.add_argument("--lang")
|
|
745
|
+
known, _ = pre.parse_known_args(argv)
|
|
746
|
+
if known.lang in ("en", "zh"):
|
|
747
|
+
_LANG_ZH = known.lang == "zh"
|
|
748
|
+
|
|
749
|
+
|
|
584
750
|
def main(argv=None):
|
|
585
751
|
use_safe_stdio()
|
|
752
|
+
apply_lang_flag(argv)
|
|
586
753
|
ap = argparse.ArgumentParser(
|
|
587
754
|
prog="claude-session-sync",
|
|
588
755
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
589
756
|
description=t(
|
|
590
757
|
"Sync local Claude Code CLI sessions into the Claude desktop app's Code sidebar (multi-account).\n"
|
|
591
758
|
"Archived sessions and sessions deleted in the desktop app are skipped. "
|
|
592
|
-
"Previews by default; pass --apply to write
|
|
759
|
+
"Previews by default; pass --apply to write.\n"
|
|
760
|
+
# 这一行固定用中文:给终端设成英文的中文用户看
|
|
761
|
+
"中文输出:加 --lang zh,或设置环境变量 CLAUDE_SESSION_SYNC_LANG=zh",
|
|
593
762
|
"把本地 Claude Code CLI 会话同步到 Claude 桌面端 Code 标签页的侧边栏(支持多账号)。\n"
|
|
594
|
-
"自动排除已归档、桌面端已删除的会话。默认只预览,加 --apply
|
|
763
|
+
"自动排除已归档、桌面端已删除的会话。默认只预览,加 --apply 才写入。\n"
|
|
764
|
+
"English output: pass --lang en, or set CLAUDE_SESSION_SYNC_LANG=en",
|
|
595
765
|
),
|
|
596
766
|
epilog=t(
|
|
597
767
|
"""\
|
|
@@ -612,7 +782,7 @@ Notes:
|
|
|
612
782
|
* Quit the desktop app completely before writing, and reopen it afterwards.
|
|
613
783
|
* Journals and backups live in ~/.claude-desktop-sync/; --undo steps back one run at a time.
|
|
614
784
|
* The desktop index format is undocumented and may break with desktop updates.
|
|
615
|
-
* Output language
|
|
785
|
+
* Output language: --lang, then CLAUDE_SESSION_SYNC_LANG, then LC_ALL / LC_MESSAGES / LANG.
|
|
616
786
|
""",
|
|
617
787
|
"""\
|
|
618
788
|
常用流程:
|
|
@@ -632,11 +802,19 @@ Notes:
|
|
|
632
802
|
* 写入前请彻底退出桌面端(菜单栏 ⌘Q),写完重新打开。
|
|
633
803
|
* 日志与备份在 ~/.claude-desktop-sync/,--undo 按时间倒序逐次撤销。
|
|
634
804
|
* 桌面端索引格式无官方文档,桌面端更新后可能失效。
|
|
635
|
-
*
|
|
805
|
+
* 输出语言:依次看 --lang、CLAUDE_SESSION_SYNC_LANG、LC_ALL / LC_MESSAGES / LANG。
|
|
636
806
|
""",
|
|
637
807
|
),
|
|
638
808
|
)
|
|
639
809
|
ap.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
810
|
+
ap.add_argument(
|
|
811
|
+
"--lang",
|
|
812
|
+
choices=["en", "zh"],
|
|
813
|
+
help=t(
|
|
814
|
+
"output language (default: from CLAUDE_SESSION_SYNC_LANG or the locale)",
|
|
815
|
+
"输出语言(默认:按 CLAUDE_SESSION_SYNC_LANG 或系统语言)",
|
|
816
|
+
),
|
|
817
|
+
)
|
|
640
818
|
ap.add_argument(
|
|
641
819
|
"--apply", action="store_true", help=t("actually write (default: preview only)", "实际写入(默认只预览)")
|
|
642
820
|
)
|
|
@@ -649,7 +827,21 @@ Notes:
|
|
|
649
827
|
ap.add_argument(
|
|
650
828
|
"--account",
|
|
651
829
|
action="append",
|
|
652
|
-
help=t(
|
|
830
|
+
help=t(
|
|
831
|
+
"only sync this account; an ID prefix such as the 8 characters shown in the output works, "
|
|
832
|
+
"add /<org-id-prefix> to pick one organization (repeatable)",
|
|
833
|
+
"只同步指定账号;可以只写输出里显示的前 8 位,加 /<组织ID前缀> 可只选一个组织(可多次使用)",
|
|
834
|
+
),
|
|
835
|
+
)
|
|
836
|
+
ap.add_argument(
|
|
837
|
+
"--set-alias",
|
|
838
|
+
action="append",
|
|
839
|
+
metavar="ID=NAME",
|
|
840
|
+
help=t(
|
|
841
|
+
"give an account a display name, e.g. b8073c71=work (ID prefix, optionally /<org-id-prefix>); "
|
|
842
|
+
"an empty name removes it; repeatable, then exits",
|
|
843
|
+
"给账号起显示名称,如 b8073c71=工作号(ID 前缀,可加 /<组织ID前缀>);名称留空则删除;可多次使用,设置完即退出",
|
|
844
|
+
),
|
|
653
845
|
)
|
|
654
846
|
ap.add_argument(
|
|
655
847
|
"--project",
|
|
@@ -731,10 +923,17 @@ Notes:
|
|
|
731
923
|
)
|
|
732
924
|
|
|
733
925
|
accts = account_dirs(roots)
|
|
734
|
-
if args.account:
|
|
735
|
-
accts = [a for a in accts if a[1] in args.account]
|
|
736
926
|
if not accts:
|
|
737
927
|
sys.exit(t("No account directories found.", "没有找到任何账号目录。"))
|
|
928
|
+
if args.set_alias:
|
|
929
|
+
set_aliases(args.set_alias, accts)
|
|
930
|
+
return
|
|
931
|
+
if args.account:
|
|
932
|
+
wanted = [resolve_account_key(v, accts) for v in args.account]
|
|
933
|
+
accts = [a for a in accts if any(a[1] == ac and (og is None or a[2] == og) for ac, og in wanted)]
|
|
934
|
+
|
|
935
|
+
book = load_account_book()
|
|
936
|
+
cli_acct = cli_login_account()
|
|
738
937
|
|
|
739
938
|
excluded = set(args.exclude)
|
|
740
939
|
if args.exclude_file:
|
|
@@ -781,7 +980,14 @@ Notes:
|
|
|
781
980
|
if args.show_template:
|
|
782
981
|
for _d, (acct, org, entries, _, _) in indexes.items():
|
|
783
982
|
tpl = pick_template(entries)
|
|
784
|
-
|
|
983
|
+
label = account_label(acct, org, book, cli_acct)
|
|
984
|
+
label = f" {label}" if label else ""
|
|
985
|
+
print(
|
|
986
|
+
t(
|
|
987
|
+
f"\n[{acct}/{org}]{label} {len(entries)} index file(s)",
|
|
988
|
+
f"\n[{acct}/{org}]{label} {len(entries)} 个索引文件",
|
|
989
|
+
)
|
|
990
|
+
)
|
|
785
991
|
if tpl:
|
|
786
992
|
print(
|
|
787
993
|
json.dumps(
|
|
@@ -822,6 +1028,8 @@ Notes:
|
|
|
822
1028
|
|
|
823
1029
|
if args.apply:
|
|
824
1030
|
ensure_desktop_stopped(args.force)
|
|
1031
|
+
if remember_cli_account(book, cli_acct):
|
|
1032
|
+
save_account_book(book)
|
|
825
1033
|
|
|
826
1034
|
created = []
|
|
827
1035
|
for d, (acct, org, entries, known, archived) in indexes.items():
|
|
@@ -834,8 +1042,8 @@ Notes:
|
|
|
834
1042
|
]
|
|
835
1043
|
print(
|
|
836
1044
|
t(
|
|
837
|
-
f"
|
|
838
|
-
f"
|
|
1045
|
+
f"{account_heading(acct, org, book, cli_acct)} existing {len(known)}, to add {len(todo)}",
|
|
1046
|
+
f"{account_heading(acct, org, book, cli_acct)} 已有 {len(known)},待补 {len(todo)}",
|
|
839
1047
|
)
|
|
840
1048
|
)
|
|
841
1049
|
if not template:
|
|
@@ -11,6 +11,8 @@ def env(tmp_path, monkeypatch):
|
|
|
11
11
|
monkeypatch.setattr(cli, "JOURNAL_DIR", e.journal)
|
|
12
12
|
# 默认假装桌面端没在运行,避免开发机上开着桌面端时测试失败
|
|
13
13
|
monkeypatch.setattr(cli, "desktop_running", lambda: False)
|
|
14
|
+
# 不读取开发机上真实的 ~/.claude.json,需要的测试自己设置
|
|
15
|
+
monkeypatch.setattr(cli, "cli_login_account", lambda: None)
|
|
14
16
|
return e
|
|
15
17
|
|
|
16
18
|
|
|
@@ -411,3 +411,136 @@ def test_include_recent_does_not_affect_purge(env):
|
|
|
411
411
|
jsonl = env.add_cli_session("busy", mtime=time.time())
|
|
412
412
|
env.run("--purge-archived", "--apply", "--include-recent")
|
|
413
413
|
assert arch.exists() and jsonl.exists()
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------- --lang
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_lang_flag_switches_help_to_chinese(monkeypatch, capsys):
|
|
420
|
+
monkeypatch.setenv("CLAUDE_SESSION_SYNC_LANG", "en") # --lang 优先于环境变量
|
|
421
|
+
with pytest.raises(SystemExit):
|
|
422
|
+
cli.main(["--lang", "zh", "--help"])
|
|
423
|
+
out = capsys.readouterr().out
|
|
424
|
+
assert "实际写入" in out and "English output: pass --lang en" in out
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def test_english_help_shows_chinese_hint(capsys):
|
|
428
|
+
with pytest.raises(SystemExit):
|
|
429
|
+
cli.main(["--help"])
|
|
430
|
+
out = capsys.readouterr().out
|
|
431
|
+
assert "actually write" in out
|
|
432
|
+
assert "中文输出:加 --lang zh" in out
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def test_lang_flag_switches_run_output(env, capsys):
|
|
436
|
+
env.add_index("existing")
|
|
437
|
+
env.add_cli_session("new-1")
|
|
438
|
+
env.run("--lang=zh")
|
|
439
|
+
assert "预览模式" in capsys.readouterr().out
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_invalid_lang_is_rejected(capsys):
|
|
443
|
+
with pytest.raises(SystemExit) as e:
|
|
444
|
+
cli.main(["--lang", "fr"])
|
|
445
|
+
assert e.value.code == 2
|
|
446
|
+
assert "invalid choice" in capsys.readouterr().err
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
# ---------------------------------------------------------------- 账号名称、别名、前缀
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
CLI_ACCT = {"account": ACCT, "org": ORG, "email": "alice@example.com", "org_name": "alice@example.com's Organization"}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@pytest.mark.parametrize(
|
|
456
|
+
("email", "masked"),
|
|
457
|
+
[("alice@example.com", "al***@example.com"), ("ab@x.io", "a***@x.io"), ("a@x.io", "a***@x.io")],
|
|
458
|
+
)
|
|
459
|
+
def test_mask_email(email, masked):
|
|
460
|
+
assert cli.mask_email(email) == masked
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def test_cli_login_account_reads_only_needed_fields(tmp_path, monkeypatch):
|
|
464
|
+
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(tmp_path))
|
|
465
|
+
(tmp_path / ".claude.json").write_text(
|
|
466
|
+
json.dumps(
|
|
467
|
+
{
|
|
468
|
+
"oauthAccount": {
|
|
469
|
+
"accountUuid": ACCT,
|
|
470
|
+
"organizationUuid": ORG,
|
|
471
|
+
"emailAddress": "alice@example.com",
|
|
472
|
+
"organizationName": "Acme",
|
|
473
|
+
"billingType": "secret-ish",
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
assert cli.cli_login_account() == {"account": ACCT, "org": ORG, "email": "alice@example.com", "org_name": "Acme"}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def test_heading_shows_masked_cli_account(env, monkeypatch, capsys):
|
|
482
|
+
monkeypatch.setattr(cli, "cli_login_account", lambda: dict(CLI_ACCT))
|
|
483
|
+
env.add_index("existing")
|
|
484
|
+
env.org_dir(ACCT, ORG_EMPTY)
|
|
485
|
+
env.run()
|
|
486
|
+
out = capsys.readouterr().out
|
|
487
|
+
assert "[al***@example.com · current CLI login] (aaaaaaaa…/bbbbbbbb…)" in out
|
|
488
|
+
assert "alice@example.com" not in out # 完整邮箱不能出现,组织名里的也不行
|
|
489
|
+
assert "[aaaaaaaa…/bbbbbbbb…] existing 0" in out # 另一个组织没有名称
|
|
490
|
+
assert not env.journal.exists() # 预览不写账号记录
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def test_apply_remembers_cli_account(env, monkeypatch, capsys):
|
|
494
|
+
monkeypatch.setattr(cli, "cli_login_account", lambda: dict(CLI_ACCT, org_name="Acme"))
|
|
495
|
+
env.add_index("existing")
|
|
496
|
+
env.run("--apply")
|
|
497
|
+
saved = json.loads((env.journal / "accounts.json").read_text())
|
|
498
|
+
assert saved["accounts"][f"{ACCT}/{ORG}"]["email"] == "alice@example.com"
|
|
499
|
+
# 之后切换到别的 CLI 账号,仍然能显示记下来的名称
|
|
500
|
+
monkeypatch.setattr(cli, "cli_login_account", lambda: None)
|
|
501
|
+
capsys.readouterr()
|
|
502
|
+
env.run()
|
|
503
|
+
assert "[Acme · al***@example.com] (aaaaaaaa…/bbbbbbbb…)" in capsys.readouterr().out
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def test_set_alias_and_remove(env, capsys):
|
|
507
|
+
env.add_index("existing")
|
|
508
|
+
env.org_dir(ACCT, ORG_EMPTY)
|
|
509
|
+
env.run("--set-alias", "aaaaaaaa=work") # 只写账号前缀:对该账号的所有组织生效
|
|
510
|
+
env.run("--set-alias", "aaaaaaaa/bbbbbbbb-0000-4000-8000-000000000002=side") # 组织级别优先
|
|
511
|
+
capsys.readouterr()
|
|
512
|
+
env.run()
|
|
513
|
+
out = capsys.readouterr().out
|
|
514
|
+
assert "[work] (aaaaaaaa…/bbbbbbbb…) existing 1" in out
|
|
515
|
+
assert "[side] (aaaaaaaa…/bbbbbbbb…) existing 0" in out
|
|
516
|
+
env.run("--set-alias", "aaaaaaaa=")
|
|
517
|
+
saved = json.loads((env.journal / "accounts.json").read_text())
|
|
518
|
+
assert ACCT not in saved["aliases"] and f"{ACCT}/{ORG_EMPTY}" in saved["aliases"]
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_set_alias_errors(env):
|
|
522
|
+
env.add_index("existing")
|
|
523
|
+
with pytest.raises(SystemExit) as e:
|
|
524
|
+
env.run("--set-alias", "zzzz=x")
|
|
525
|
+
assert "No account matches" in str(e.value.code)
|
|
526
|
+
with pytest.raises(SystemExit) as e:
|
|
527
|
+
env.run("--set-alias", "aaaaaaaa")
|
|
528
|
+
assert "ID=NAME" in str(e.value.code)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_account_prefix_filter(env):
|
|
532
|
+
other = "cccccccc-0000-4000-8000-000000000001"
|
|
533
|
+
env.add_index("existing")
|
|
534
|
+
env.add_index("existing", acct=other)
|
|
535
|
+
env.add_cli_session("new-1")
|
|
536
|
+
env.run("--apply", "--account", "cccccccc")
|
|
537
|
+
assert cli_ids(env.index_entries(other, ORG)) == ["existing", "new-1"]
|
|
538
|
+
assert cli_ids(env.index_entries(ACCT, ORG)) == ["existing"]
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_ambiguous_account_prefix_is_rejected(env):
|
|
542
|
+
env.add_index("existing")
|
|
543
|
+
env.add_index("existing", acct="aaaaaaaa-1111-4000-8000-000000000001")
|
|
544
|
+
with pytest.raises(SystemExit) as e:
|
|
545
|
+
env.run("--account", "aaaaaaaa")
|
|
546
|
+
assert "matches several accounts" in str(e.value.code)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/src/claude_session_sync/__init__.py
RENAMED
|
File without changes
|
{claude_session_sync_cli-0.1.2 → claude_session_sync_cli-0.2.0}/src/claude_session_sync/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|