multi-codex 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {multi_codex-0.8.0/src/multi_codex.egg-info → multi_codex-0.9.0}/PKG-INFO +25 -6
- {multi_codex-0.8.0 → multi_codex-0.9.0}/README.md +24 -5
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/__init__.py +1 -1
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/accounts.py +31 -4
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/apps.py +8 -5
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/cli.py +118 -8
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/completion.py +2 -2
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/config.py +70 -3
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/doctor.py +3 -2
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/launcher.py +7 -1
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/migrate.py +2 -2
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/platform.py +2 -1
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/shared.py +4 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/switch.py +5 -1
- {multi_codex-0.8.0 → multi_codex-0.9.0/src/multi_codex.egg-info}/PKG-INFO +25 -6
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex.egg-info/SOURCES.txt +1 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_ergonomics.py +11 -8
- multi_codex-0.9.0/tests/test_isolation.py +428 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/LICENSE +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/pyproject.toml +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/setup.cfg +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/__main__.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/actions.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/binding.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/fsutil.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/identity.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/lock.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/shellpath.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex/usage.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex.egg-info/dependency_links.txt +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex.egg-info/entry_points.txt +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/src/multi_codex.egg-info/top_level.txt +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_accounts.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_apps.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_binding.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_config.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_config_copy.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_everyday.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_fsutil.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_insight.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_install.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_migrate.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_onboarding.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_onboarding_commands.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_platform.py +0 -0
- {multi_codex-0.8.0 → multi_codex-0.9.0}/tests/test_switch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multi-codex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Run several Codex CLI accounts side by side: separate CODEX_HOME directories, launchers and proxies.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/jakoes-wu/multi-codex
|
|
@@ -142,6 +142,7 @@ Without a name, the account is named after the e-mail address in `~/.codex/auth.
|
|
|
142
142
|
| Set up all accounts on a new machine | `curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh \| sh -s -- --config accounts.json` | [Declarative setup](#declarative-setup-with-apply) |
|
|
143
143
|
| Get tab completion | `eval "$(multi-codex completion zsh)"` | [Shell completion](#shell-completion) |
|
|
144
144
|
| See what a command would change | add `--dry-run` | [Commands](#commands) |
|
|
145
|
+
| Rename an account | `multi-codex rename work client-a` (the directory and login stay) | [Renaming accounts](#renaming-accounts) |
|
|
145
146
|
| Remove an account | `multi-codex remove work` (the directory is kept) | [Commands](#commands) |
|
|
146
147
|
|
|
147
148
|
More questions are answered in the [FAQ](#faq).
|
|
@@ -159,8 +160,9 @@ More questions are answered in the [FAQ](#faq).
|
|
|
159
160
|
| ---- | ---- |
|
|
160
161
|
| `multi-codex init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
161
162
|
| `multi-codex migrate-default [NAME] [--source DIR] [--copy] [--keep-backup] [--proxy P] [--skip-process-check] [--accept-relogin]` | Turn the default directory into an account. Without NAME, the e-mail address in its `auth.json` is used. |
|
|
162
|
-
| `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
|
|
163
|
-
| `multi-codex set NAME [
|
|
163
|
+
| `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--shared-exclude ITEM] [--shared-include ITEM] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
|
|
164
|
+
| `multi-codex set NAME [same options as add]` | Change an existing account; same options as `add`, but never creates one. |
|
|
165
|
+
| `multi-codex rename OLD NEW` | Rename an account and its launcher; the directory, login and links stay. |
|
|
164
166
|
| `multi-codex login NAME [-- ARGS]` | Run `codex login` with an account's environment; arguments after `--` go to `codex login`. Does not need `~/.local/bin` on `PATH`. |
|
|
165
167
|
| `multi-codex proxy NAME PORT\|URL\|off\|inherit` | Set an account's proxy. |
|
|
166
168
|
| `multi-codex remove NAME` | Unregister an account and delete its launcher. **The account directory is kept.** |
|
|
@@ -178,7 +180,7 @@ More questions are answered in the [FAQ](#faq).
|
|
|
178
180
|
| `multi-codex restore NAME [--skip-process-check] [--accept-relogin]` | Undo `migrate-default`: move the account back to `~/.codex`. |
|
|
179
181
|
| `multi-codex completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
180
182
|
|
|
181
|
-
Every write command accepts `--dry-run`. `init`, `add`, `set`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
|
|
183
|
+
Every write command accepts `--dry-run`. `init`, `add`, `set`, `rename`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
|
|
182
184
|
|
|
183
185
|
### Login and usage
|
|
184
186
|
|
|
@@ -233,6 +235,14 @@ multi-codex unbind # remove the binding of the curre
|
|
|
233
235
|
|
|
234
236
|
`multi-codex add new --config-from work` copies `config.toml` from `work` into `new` once; afterwards the two files are independent. An existing `config.toml` with different content is a conflict (nothing is written), and so is copying into an account that shares `config.toml`. The copy includes everything in the file, such as `cli_auth_credentials_store` or absolute paths that point into the other account.
|
|
235
237
|
|
|
238
|
+
### Renaming accounts
|
|
239
|
+
|
|
240
|
+
```sh
|
|
241
|
+
multi-codex rename work client-a
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The account and its launcher get the new name (`codex-client-a`; `codex-work` is deleted), and directory bindings and the default account follow. The directory stays where it is, so the login, sessions and shared links are kept; `config.json` records it as `"dir": "work"` (in `list --json`, `dir` is the full path instead). A name that is still used as another account's directory, such as `work` after this rename, cannot be given to a new account. Renaming only the letter case is not supported. Before downgrading to a version older than 0.9, rename the account back: older versions ignore `dir` and would use `<root>/client-a`.
|
|
245
|
+
|
|
236
246
|
### Per-account environment variables
|
|
237
247
|
|
|
238
248
|
```sh
|
|
@@ -242,6 +252,8 @@ multi-codex env work --unset TERM_PROGRAM
|
|
|
242
252
|
multi-codex env work --clear
|
|
243
253
|
```
|
|
244
254
|
|
|
255
|
+
Launchers (and `run`, `login`, `code` and `usage --live`, which use the same environment) clear `CODEX_API_KEY`, `CODEX_ACCESS_TOKEN` and `CODEX_SQLITE_HOME` inherited from your shell, so one exported API key or database location does not leak into every account. An account that needs an API key sets it here, for example `multi-codex env work CODEX_API_KEY=sk-…`; per-account variables are applied after the clearing.
|
|
256
|
+
|
|
245
257
|
The variables are stored in `config.json` (`accounts.<name>.env`) and written into the launcher. Values are used literally (no `$VAR` expansion). `CODEX_HOME` and the proxy variables are reserved: use `multi-codex proxy` for proxies. A launcher with environment variables is readable only by you (mode 0700), and `list --json` shows only the variable names; still, the values are stored in plain text, so do not put secrets there that need stronger protection. Older versions of multi-codex ignore the `env` field and drop it on their next write; run `multi-codex env NAME --clear` before downgrading.
|
|
246
258
|
|
|
247
259
|
### VS Code and the desktop app (experimental)
|
|
@@ -327,7 +339,7 @@ Progress is recorded in `~/.config/multi-codex/migrate-journal.json`. If the mig
|
|
|
327
339
|
|
|
328
340
|
Sockets and FIFOs (runtime files such as `ipc.sock`) are not copied in copy mode. On macOS, copy mode does not preserve extended attributes.
|
|
329
341
|
|
|
330
|
-
If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you:
|
|
342
|
+
If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you: launchers set or clear them, but plain `codex` still uses them.
|
|
331
343
|
|
|
332
344
|
### Undoing a migration
|
|
333
345
|
|
|
@@ -361,6 +373,10 @@ multi-codex creates the missing links and remembers which links it created. Turn
|
|
|
361
373
|
|
|
362
374
|
If you already linked an account to the shared directory by hand, `multi-codex set NAME --shared --adopt` takes those links over without recreating them: from then on, turning sharing off removes them as well. Only links that already point to the matching shared item are adopted.
|
|
363
375
|
|
|
376
|
+
One account can opt out of some shared items: `multi-codex set work --shared-exclude skills` removes the `skills` link multi-codex created in `work` (as if sharing were off for that item) and no longer creates it; `--shared-include skills` undoes that. Both options can be repeated, names match `shared.items` exactly, and `list` shows the exclusions as `yes (not: skills)`. To exclude `config.toml` and then copy another account's settings into it, run `--shared-exclude config.toml` and `--config-from` as two separate commands.
|
|
377
|
+
|
|
378
|
+
Items that hold one account's own state can never be shared: `shared.items` must not contain the entries marked "No" in the table below. A configuration that lists one fails to load and names the item; remove it from `config.json`.
|
|
379
|
+
|
|
364
380
|
### What can be shared
|
|
365
381
|
|
|
366
382
|
Based on the Codex source code (openai/codex at `6b4daafd`):
|
|
@@ -375,7 +391,7 @@ Based on the Codex source code (openai/codex at `6b4daafd`):
|
|
|
375
391
|
| `history.jsonl` | Prompt history | Yes | Reads and writes are file-locked; the histories of the accounts are merged. |
|
|
376
392
|
| `auth.json`, `secrets/`, `.credentials.json`, `.env` | Credentials | **No** | They are the account. |
|
|
377
393
|
| `installation_id` | Installation identifier | No | Sent with requests; sharing makes several accounts look like one installation. |
|
|
378
|
-
| `*.sqlite` (`state_5.sqlite`, …) | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
|
|
394
|
+
| `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm` (`state_5.sqlite`, …), `sqlite/` | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
|
|
379
395
|
| `sessions/`, `archived_sessions/`, `session_index.jsonl` | Session logs | No | The index has only an in-process lock; sessions record the account that created them. |
|
|
380
396
|
| `models_cache.json`, `cache/` | Caches | Not needed | Keyed by the account; a mismatch is a cache miss. |
|
|
381
397
|
| `app-server-control/`, `app-server-daemon/`, `packages/`, `tmp/`, `.tmp/`, `log/`, `shell_snapshots/` | Runtime state | No | Per process or per session. |
|
|
@@ -438,8 +454,11 @@ Run the installer again; configuration, accounts and launchers are not touched:
|
|
|
438
454
|
```sh
|
|
439
455
|
curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh | sh
|
|
440
456
|
multi-codex --version
|
|
457
|
+
multi-codex apply # after upgrading from 0.8 or older: rewrites the launchers
|
|
441
458
|
```
|
|
442
459
|
|
|
460
|
+
0.9 changed what the launchers contain. Until `multi-codex apply` (or any other write command) rewrites them, `list` shows `launcher stale` and `usage --live` refuses to run; the old launchers keep working.
|
|
461
|
+
|
|
443
462
|
## Exit codes
|
|
444
463
|
|
|
445
464
|
| Code | Meaning |
|
|
@@ -118,6 +118,7 @@ Without a name, the account is named after the e-mail address in `~/.codex/auth.
|
|
|
118
118
|
| Set up all accounts on a new machine | `curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh \| sh -s -- --config accounts.json` | [Declarative setup](#declarative-setup-with-apply) |
|
|
119
119
|
| Get tab completion | `eval "$(multi-codex completion zsh)"` | [Shell completion](#shell-completion) |
|
|
120
120
|
| See what a command would change | add `--dry-run` | [Commands](#commands) |
|
|
121
|
+
| Rename an account | `multi-codex rename work client-a` (the directory and login stay) | [Renaming accounts](#renaming-accounts) |
|
|
121
122
|
| Remove an account | `multi-codex remove work` (the directory is kept) | [Commands](#commands) |
|
|
122
123
|
|
|
123
124
|
More questions are answered in the [FAQ](#faq).
|
|
@@ -135,8 +136,9 @@ More questions are answered in the [FAQ](#faq).
|
|
|
135
136
|
| ---- | ---- |
|
|
136
137
|
| `multi-codex init [--root DIR] [--bin-dir DIR] [--shared-dir DIR] [--shared-items A,B]` | Create or change global settings. |
|
|
137
138
|
| `multi-codex migrate-default [NAME] [--source DIR] [--copy] [--keep-backup] [--proxy P] [--skip-process-check] [--accept-relogin]` | Turn the default directory into an account. Without NAME, the e-mail address in its `auth.json` is used. |
|
|
138
|
-
| `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
|
|
139
|
-
| `multi-codex set NAME [
|
|
139
|
+
| `multi-codex add NAME [--proxy P] [--shared [DIR] \| --no-shared] [--shared-exclude ITEM] [--shared-include ITEM] [--adopt] [--config-from OTHER]` | Add an account, adopt an existing directory, or change its options. `--config-from` copies `config.toml` from another account once. |
|
|
140
|
+
| `multi-codex set NAME [same options as add]` | Change an existing account; same options as `add`, but never creates one. |
|
|
141
|
+
| `multi-codex rename OLD NEW` | Rename an account and its launcher; the directory, login and links stay. |
|
|
140
142
|
| `multi-codex login NAME [-- ARGS]` | Run `codex login` with an account's environment; arguments after `--` go to `codex login`. Does not need `~/.local/bin` on `PATH`. |
|
|
141
143
|
| `multi-codex proxy NAME PORT\|URL\|off\|inherit` | Set an account's proxy. |
|
|
142
144
|
| `multi-codex remove NAME` | Unregister an account and delete its launcher. **The account directory is kept.** |
|
|
@@ -154,7 +156,7 @@ More questions are answered in the [FAQ](#faq).
|
|
|
154
156
|
| `multi-codex restore NAME [--skip-process-check] [--accept-relogin]` | Undo `migrate-default`: move the account back to `~/.codex`. |
|
|
155
157
|
| `multi-codex completion bash\|zsh\|fish` | Print a shell completion script. |
|
|
156
158
|
|
|
157
|
-
Every write command accepts `--dry-run`. `init`, `add`, `set`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
|
|
159
|
+
Every write command accepts `--dry-run`. `init`, `add`, `set`, `rename`, `proxy`, `remove`, `apply`, `bind`, `unbind` and `env` print only the items they change (or `already up to date`); `-v` / `--verbose` also prints unchanged items. For `list`, `-v` means the full table instead. `list`, `usage` and `doctor` never change anything; with `--json` they print a single JSON object on stdout (with a `"version": 1` field) and keep warnings on stderr. Use `--json` in scripts: the table layout is not guaranteed to stay the same.
|
|
158
160
|
|
|
159
161
|
### Login and usage
|
|
160
162
|
|
|
@@ -209,6 +211,14 @@ multi-codex unbind # remove the binding of the curre
|
|
|
209
211
|
|
|
210
212
|
`multi-codex add new --config-from work` copies `config.toml` from `work` into `new` once; afterwards the two files are independent. An existing `config.toml` with different content is a conflict (nothing is written), and so is copying into an account that shares `config.toml`. The copy includes everything in the file, such as `cli_auth_credentials_store` or absolute paths that point into the other account.
|
|
211
213
|
|
|
214
|
+
### Renaming accounts
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
multi-codex rename work client-a
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The account and its launcher get the new name (`codex-client-a`; `codex-work` is deleted), and directory bindings and the default account follow. The directory stays where it is, so the login, sessions and shared links are kept; `config.json` records it as `"dir": "work"` (in `list --json`, `dir` is the full path instead). A name that is still used as another account's directory, such as `work` after this rename, cannot be given to a new account. Renaming only the letter case is not supported. Before downgrading to a version older than 0.9, rename the account back: older versions ignore `dir` and would use `<root>/client-a`.
|
|
221
|
+
|
|
212
222
|
### Per-account environment variables
|
|
213
223
|
|
|
214
224
|
```sh
|
|
@@ -218,6 +228,8 @@ multi-codex env work --unset TERM_PROGRAM
|
|
|
218
228
|
multi-codex env work --clear
|
|
219
229
|
```
|
|
220
230
|
|
|
231
|
+
Launchers (and `run`, `login`, `code` and `usage --live`, which use the same environment) clear `CODEX_API_KEY`, `CODEX_ACCESS_TOKEN` and `CODEX_SQLITE_HOME` inherited from your shell, so one exported API key or database location does not leak into every account. An account that needs an API key sets it here, for example `multi-codex env work CODEX_API_KEY=sk-…`; per-account variables are applied after the clearing.
|
|
232
|
+
|
|
221
233
|
The variables are stored in `config.json` (`accounts.<name>.env`) and written into the launcher. Values are used literally (no `$VAR` expansion). `CODEX_HOME` and the proxy variables are reserved: use `multi-codex proxy` for proxies. A launcher with environment variables is readable only by you (mode 0700), and `list --json` shows only the variable names; still, the values are stored in plain text, so do not put secrets there that need stronger protection. Older versions of multi-codex ignore the `env` field and drop it on their next write; run `multi-codex env NAME --clear` before downgrading.
|
|
222
234
|
|
|
223
235
|
### VS Code and the desktop app (experimental)
|
|
@@ -303,7 +315,7 @@ Progress is recorded in `~/.config/multi-codex/migrate-journal.json`. If the mig
|
|
|
303
315
|
|
|
304
316
|
Sockets and FIFOs (runtime files such as `ipc.sock`) are not copied in copy mode. On macOS, copy mode does not preserve extended attributes.
|
|
305
317
|
|
|
306
|
-
If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you:
|
|
318
|
+
If `CODEX_HOME`, `CODEX_SQLITE_HOME`, `CODEX_API_KEY` or `CODEX_ACCESS_TOKEN` is set in your environment, multi-codex warns you: launchers set or clear them, but plain `codex` still uses them.
|
|
307
319
|
|
|
308
320
|
### Undoing a migration
|
|
309
321
|
|
|
@@ -337,6 +349,10 @@ multi-codex creates the missing links and remembers which links it created. Turn
|
|
|
337
349
|
|
|
338
350
|
If you already linked an account to the shared directory by hand, `multi-codex set NAME --shared --adopt` takes those links over without recreating them: from then on, turning sharing off removes them as well. Only links that already point to the matching shared item are adopted.
|
|
339
351
|
|
|
352
|
+
One account can opt out of some shared items: `multi-codex set work --shared-exclude skills` removes the `skills` link multi-codex created in `work` (as if sharing were off for that item) and no longer creates it; `--shared-include skills` undoes that. Both options can be repeated, names match `shared.items` exactly, and `list` shows the exclusions as `yes (not: skills)`. To exclude `config.toml` and then copy another account's settings into it, run `--shared-exclude config.toml` and `--config-from` as two separate commands.
|
|
353
|
+
|
|
354
|
+
Items that hold one account's own state can never be shared: `shared.items` must not contain the entries marked "No" in the table below. A configuration that lists one fails to load and names the item; remove it from `config.json`.
|
|
355
|
+
|
|
340
356
|
### What can be shared
|
|
341
357
|
|
|
342
358
|
Based on the Codex source code (openai/codex at `6b4daafd`):
|
|
@@ -351,7 +367,7 @@ Based on the Codex source code (openai/codex at `6b4daafd`):
|
|
|
351
367
|
| `history.jsonl` | Prompt history | Yes | Reads and writes are file-locked; the histories of the accounts are merged. |
|
|
352
368
|
| `auth.json`, `secrets/`, `.credentials.json`, `.env` | Credentials | **No** | They are the account. |
|
|
353
369
|
| `installation_id` | Installation identifier | No | Sent with requests; sharing makes several accounts look like one installation. |
|
|
354
|
-
| `*.sqlite` (`state_5.sqlite`, …) | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
|
|
370
|
+
| `*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm` (`state_5.sqlite`, …), `sqlite/` | Threads, logs, memories | **No** | `state_5.sqlite` records account IDs. |
|
|
355
371
|
| `sessions/`, `archived_sessions/`, `session_index.jsonl` | Session logs | No | The index has only an in-process lock; sessions record the account that created them. |
|
|
356
372
|
| `models_cache.json`, `cache/` | Caches | Not needed | Keyed by the account; a mismatch is a cache miss. |
|
|
357
373
|
| `app-server-control/`, `app-server-daemon/`, `packages/`, `tmp/`, `.tmp/`, `log/`, `shell_snapshots/` | Runtime state | No | Per process or per session. |
|
|
@@ -414,8 +430,11 @@ Run the installer again; configuration, accounts and launchers are not touched:
|
|
|
414
430
|
```sh
|
|
415
431
|
curl -fsSL https://raw.githubusercontent.com/jakoes-wu/multi-codex/main/install.sh | sh
|
|
416
432
|
multi-codex --version
|
|
433
|
+
multi-codex apply # after upgrading from 0.8 or older: rewrites the launchers
|
|
417
434
|
```
|
|
418
435
|
|
|
436
|
+
0.9 changed what the launchers contain. Until `multi-codex apply` (or any other write command) rewrites them, `list` shows `launcher stale` and `usage --live` refuses to run; the old launchers keep working.
|
|
437
|
+
|
|
419
438
|
## Exit codes
|
|
420
439
|
|
|
421
440
|
| Code | Meaning |
|
|
@@ -32,7 +32,27 @@ NO_ORPHANS: FrozenSet[str] = frozenset()
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def account_dir(config: Config, name: str) -> str:
|
|
35
|
-
|
|
35
|
+
"""账号目录。用账号记录的目录名(rename 后与账号名不同);未登记的名字按名字本身拼。"""
|
|
36
|
+
account = config.find(name)
|
|
37
|
+
return os.path.join(expand(config.root), account.dir_name if account is not None else name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dir_conflicts(config: Config) -> List[str]:
|
|
41
|
+
"""两个账号会落到同一个目录时的说明文字(规则同 config._check_dir_names)。
|
|
42
|
+
|
|
43
|
+
parse_config 只检查读入的文件;这里检查命令算出的新配置,典型是 rename work job 之后 add work。
|
|
44
|
+
"""
|
|
45
|
+
problems = []
|
|
46
|
+
for account in config.accounts.values():
|
|
47
|
+
for other in config.accounts.values():
|
|
48
|
+
if other is account:
|
|
49
|
+
continue
|
|
50
|
+
if account.dir_name.casefold() == other.dir_name.casefold() or \
|
|
51
|
+
account.name.casefold() == other.dir_name.casefold():
|
|
52
|
+
problems.append("account {!r} would use the directory of account {!r}".format(
|
|
53
|
+
account.name, other.name))
|
|
54
|
+
break
|
|
55
|
+
return problems
|
|
36
56
|
|
|
37
57
|
|
|
38
58
|
def plan(old: Config, new: Config, *, config_exists: bool = True,
|
|
@@ -60,6 +80,9 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
|
|
|
60
80
|
"cannot change root from {} to {} while accounts are registered".format(
|
|
61
81
|
old_root, new_root)))
|
|
62
82
|
|
|
83
|
+
for problem in dir_conflicts(new):
|
|
84
|
+
actions.append(Action(CONFLICT, "config", config_path(), problem))
|
|
85
|
+
|
|
63
86
|
planned_deletes: Set[str] = set()
|
|
64
87
|
for account in new.accounts.values():
|
|
65
88
|
old_account = old.find(account.name)
|
|
@@ -70,7 +93,7 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
|
|
|
70
93
|
"account {!r} is registered as {!r}; renaming is not supported".format(
|
|
71
94
|
account.name, old_account.name)))
|
|
72
95
|
continue
|
|
73
|
-
directory = os.path.join(new_root, account.
|
|
96
|
+
directory = os.path.join(new_root, account.dir_name)
|
|
74
97
|
actions.extend(_plan_account_dir(directory, assumed))
|
|
75
98
|
actions.extend(_plan_launcher(new_bin, account.name, directory, account.proxy, account.env))
|
|
76
99
|
if old_account is not None and old_bin != new_bin:
|
|
@@ -79,11 +102,15 @@ def plan(old: Config, new: Config, *, config_exists: bool = True,
|
|
|
79
102
|
actions.extend(shared.plan_shared(new, account, directory, old.shared_dir,
|
|
80
103
|
adopt=account.name.casefold() in adopt_accounts))
|
|
81
104
|
|
|
105
|
+
new_dirs = {account.dir_name.casefold() for account in new.accounts.values()}
|
|
82
106
|
for old_account in old.accounts.values():
|
|
83
107
|
if new.find(old_account.name) is None:
|
|
84
108
|
actions.extend(_plan_launcher_delete(old_bin, old_account.name, planned_deletes,
|
|
85
109
|
"account removed"))
|
|
86
|
-
|
|
110
|
+
# rename:旧名字从配置里消失,但目录由新名字接着用,共享链接必须留着。
|
|
111
|
+
if old_account.dir_name.casefold() in new_dirs:
|
|
112
|
+
continue
|
|
113
|
+
actions.extend(shared.plan_remove_links(old_account, os.path.join(old_root, old_account.dir_name),
|
|
87
114
|
old.shared_dir))
|
|
88
115
|
|
|
89
116
|
if orphan_scope:
|
|
@@ -121,7 +148,7 @@ def plan_config_copy(config: Config, account: Account, content: str, source_name
|
|
|
121
148
|
已有不同内容时不覆盖:那可能是用户已经改过的配置。
|
|
122
149
|
"""
|
|
123
150
|
target = os.path.join(account_dir(config, account.name), "config.toml")
|
|
124
|
-
if account.shared and "config.toml" in config.shared_items:
|
|
151
|
+
if account.shared and "config.toml" in config.shared_items and "config.toml" not in account.shared_exclude:
|
|
125
152
|
return [Action(CONFLICT, "config-file", target,
|
|
126
153
|
"config.toml is shared for this account and does not need copying")]
|
|
127
154
|
kind = entry_kind(target)
|
|
@@ -33,15 +33,18 @@ def _private_dir(path: str) -> str:
|
|
|
33
33
|
return path
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def gui_data_dir(config: Config,
|
|
37
|
-
"""返回并创建 <root>/.apps
|
|
36
|
+
def gui_data_dir(config: Config, dir_name: str, kind: str) -> str:
|
|
37
|
+
"""返回并创建 <root>/.apps/<账号目录名>/<kind>(kind 为 vscode 或 desktop),各级目录都只允许本人访问。
|
|
38
|
+
|
|
39
|
+
按目录名而不是账号名存放:rename 后 VS Code 与桌面端的登录状态、窗口与扩展数据都还在原处。
|
|
40
|
+
"""
|
|
38
41
|
apps_root = _private_dir(os.path.join(expand(config.root), ".apps"))
|
|
39
|
-
account_root = _private_dir(os.path.join(apps_root,
|
|
42
|
+
account_root = _private_dir(os.path.join(apps_root, dir_name))
|
|
40
43
|
return _private_dir(os.path.join(account_root, kind))
|
|
41
44
|
|
|
42
45
|
|
|
43
|
-
def desktop_log(config: Config,
|
|
44
|
-
path = os.path.join(expand(config.root), ".apps",
|
|
46
|
+
def desktop_log(config: Config, dir_name: str) -> str:
|
|
47
|
+
path = os.path.join(expand(config.root), ".apps", dir_name, "desktop.log")
|
|
45
48
|
fd = os.open(path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o600)
|
|
46
49
|
os.close(fd)
|
|
47
50
|
os.chmod(path, 0o600)
|
|
@@ -16,8 +16,10 @@ from typing import List, Optional, Tuple
|
|
|
16
16
|
from . import (__version__, accounts, apps, binding, completion, doctor, identity, launcher, migrate, platform,
|
|
17
17
|
shellpath, switch, usage)
|
|
18
18
|
from .actions import CREATE, DELETE, UNCHANGED, UPDATE, Action, error, hint, info, print_action, warn
|
|
19
|
-
from .config import (DEFAULT_SHARED_DIR, DEFAULT_SHARED_ITEMS, Account, Config, ConfigError, is_socks,
|
|
20
|
-
normalize_proxy, parse_config,
|
|
19
|
+
from .config import (DEFAULT_SHARED_DIR, DEFAULT_SHARED_ITEMS, Account, Config, ConfigError, is_socks,
|
|
20
|
+
is_unshareable, load_config, normalize_proxy, parse_config, unique_items, validate_env_key,
|
|
21
|
+
validate_env_value, validate_name)
|
|
22
|
+
from .config import _valid_item as valid_shared_item # 与配置解析用同一条名称规则
|
|
21
23
|
from .fsutil import KIND_DIR, KIND_MISSING, display_path, entry_kind, expand
|
|
22
24
|
from .lock import LockBusyError, WriteLock
|
|
23
25
|
|
|
@@ -53,6 +55,7 @@ COMMAND_GROUPS = (
|
|
|
53
55
|
("Advanced", (
|
|
54
56
|
("env", "list or change an account's extra environment variables"),
|
|
55
57
|
("remove", "unregister an account (its directory is kept)"),
|
|
58
|
+
("rename", "rename an account and its launcher (the directory stays)"),
|
|
56
59
|
("restore", "undo migrate-default: move an account back to ~/.codex"),
|
|
57
60
|
("apply", "converge all accounts to the configuration"),
|
|
58
61
|
("init", "create or update the global settings"),
|
|
@@ -128,6 +131,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
128
131
|
_add_dry_run(p_proxy)
|
|
129
132
|
_add_verbose(p_proxy)
|
|
130
133
|
|
|
134
|
+
p_rename = sub.add_parser("rename", description=COMMAND_SUMMARY["rename"])
|
|
135
|
+
p_rename.add_argument("old", metavar="OLD")
|
|
136
|
+
p_rename.add_argument("new", metavar="NEW")
|
|
137
|
+
_add_dry_run(p_rename)
|
|
138
|
+
_add_verbose(p_rename)
|
|
139
|
+
|
|
131
140
|
p_remove = sub.add_parser("remove", description=COMMAND_SUMMARY["remove"])
|
|
132
141
|
p_remove.add_argument("name")
|
|
133
142
|
_add_dry_run(p_remove)
|
|
@@ -239,6 +248,11 @@ def _add_account_options(parser: argparse.ArgumentParser) -> None:
|
|
|
239
248
|
parser.add_argument("--adopt", action="store_true",
|
|
240
249
|
help="take over existing links that already point to the shared items, "
|
|
241
250
|
"so that turning sharing off later removes them too")
|
|
251
|
+
# 按账号退出 / 恢复全局共享清单中的某些项;都可重复。退出只删本工具建的链接,与关闭共享一致。
|
|
252
|
+
parser.add_argument("--shared-exclude", action="append", metavar="ITEM",
|
|
253
|
+
help="do not link this shared item into this account (repeatable)")
|
|
254
|
+
parser.add_argument("--shared-include", action="append", metavar="ITEM",
|
|
255
|
+
help="undo --shared-exclude for this item (repeatable)")
|
|
242
256
|
_add_dry_run(parser)
|
|
243
257
|
_add_verbose(parser)
|
|
244
258
|
|
|
@@ -434,6 +448,10 @@ def dispatch(args: argparse.Namespace) -> int:
|
|
|
434
448
|
items = [item.strip() for item in args.shared_items.split(",") if item.strip()]
|
|
435
449
|
if any(item in (".", "..") or "/" in item for item in items):
|
|
436
450
|
raise UsageError("--shared-items must be plain names without '/'")
|
|
451
|
+
for item in items:
|
|
452
|
+
if is_unshareable(item):
|
|
453
|
+
raise UsageError("--shared-items must not include {!r}: it holds account-specific "
|
|
454
|
+
"state".format(item))
|
|
437
455
|
new.shared_items = items
|
|
438
456
|
elif args.command in ("add", "set"):
|
|
439
457
|
# add 对已登记的账号也是“修改选项”(保持兼容);set 只改已登记的账号,不会新建。
|
|
@@ -460,6 +478,9 @@ def dispatch(args: argparse.Namespace) -> int:
|
|
|
460
478
|
account.shared = True
|
|
461
479
|
elif args.no_shared:
|
|
462
480
|
account.shared = False
|
|
481
|
+
# 放在 --config-from 之前,plan_config_copy 才能看到排除项。注意同一条命令里排除 config.toml 并复制
|
|
482
|
+
# 仍会判冲突:计划按当前文件判断,那时 config.toml 还是链到共享内容的软链;要分两条命令。
|
|
483
|
+
_apply_shared_exclude(account, args)
|
|
463
484
|
if args.adopt:
|
|
464
485
|
# 接管只对开启了共享的账号有意义;关闭状态下工具本来就不管这些软链。
|
|
465
486
|
if not account.shared:
|
|
@@ -475,6 +496,11 @@ def dispatch(args: argparse.Namespace) -> int:
|
|
|
475
496
|
error(new.not_registered(args.name))
|
|
476
497
|
return accounts.EXIT_ERROR
|
|
477
498
|
account.proxy = _checked_proxy(args.value)
|
|
499
|
+
elif args.command == "rename":
|
|
500
|
+
prepared = _rename_config(new, args)
|
|
501
|
+
if isinstance(prepared, int):
|
|
502
|
+
return prepared
|
|
503
|
+
new, orphan_scope = prepared
|
|
478
504
|
elif args.command == "remove":
|
|
479
505
|
name = _checked_name(args.name)
|
|
480
506
|
account = new.find(name)
|
|
@@ -537,6 +563,10 @@ def dispatch(args: argparse.Namespace) -> int:
|
|
|
537
563
|
info("binding not changed because of the errors above")
|
|
538
564
|
if args.command in ("add", "set") and code == accounts.EXIT_OK and not args.dry_run:
|
|
539
565
|
_hint_shared_dir(old, new, args)
|
|
566
|
+
_hint_shared_exclude(new, args)
|
|
567
|
+
if args.command == "rename" and code == accounts.EXIT_OK and not args.dry_run:
|
|
568
|
+
info("renamed {} to {}; directory {} is unchanged".format(
|
|
569
|
+
args.old, args.new, display_path(accounts.account_dir(new, args.new))))
|
|
540
570
|
# set 不新建账号,不需要“下一步登录”的提示。
|
|
541
571
|
if args.command == "add" and code == accounts.EXIT_OK and not args.dry_run:
|
|
542
572
|
_hint_after_setup(new, args.name, check_login=True)
|
|
@@ -545,7 +575,68 @@ def dispatch(args: argparse.Namespace) -> int:
|
|
|
545
575
|
|
|
546
576
|
def _any_account_option(args: argparse.Namespace) -> bool:
|
|
547
577
|
return (args.proxy is not None or args.shared_to is not None or args.no_shared or args.adopt
|
|
548
|
-
or bool(args.config_from))
|
|
578
|
+
or bool(args.config_from) or bool(args.shared_exclude) or bool(args.shared_include))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _apply_shared_exclude(account: Account, args: argparse.Namespace) -> None:
|
|
582
|
+
"""--shared-include 先撤销,--shared-exclude 再追加;同一名称同时出现在两边是用法错误。
|
|
583
|
+
|
|
584
|
+
只改配置:链接的增删由收敛计划完成(shared.plan_shared 跳过被排除的项,原来的受管链接按关闭共享删除)。
|
|
585
|
+
"""
|
|
586
|
+
exclude = args.shared_exclude or []
|
|
587
|
+
include = args.shared_include or []
|
|
588
|
+
for item in exclude + include:
|
|
589
|
+
if not valid_shared_item(item):
|
|
590
|
+
raise UsageError("shared item names must be plain names without '/', got {!r}".format(item))
|
|
591
|
+
both = sorted(set(exclude) & set(include))
|
|
592
|
+
if both:
|
|
593
|
+
raise UsageError("{} given to both --shared-exclude and --shared-include".format(", ".join(both)))
|
|
594
|
+
kept = [item for item in account.shared_exclude if item not in include]
|
|
595
|
+
account.shared_exclude = unique_items(kept + exclude)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _hint_shared_exclude(config: Config, args: argparse.Namespace) -> None:
|
|
599
|
+
"""退出项暂时不起作用的两种情况:名称不在共享清单里(精确匹配,与 plan_shared 相同),或账号没开共享。"""
|
|
600
|
+
exclude = args.shared_exclude or []
|
|
601
|
+
if not exclude:
|
|
602
|
+
return
|
|
603
|
+
account = config.find(args.name)
|
|
604
|
+
for item in exclude:
|
|
605
|
+
if item not in config.shared_items:
|
|
606
|
+
info("note: {} is not in shared.items; the exclusion takes effect only if it is added there".format(
|
|
607
|
+
item))
|
|
608
|
+
if account is not None and not account.shared:
|
|
609
|
+
info("note: sharing is off for {}; the exclusion applies once it is turned on".format(account.name))
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _rename_config(new: Config, args: argparse.Namespace):
|
|
613
|
+
"""rename 的新配置:返回 (新配置, orphan_scope),或直接返回退出码。
|
|
614
|
+
|
|
615
|
+
新账号是旧账号的完整副本(代理、共享、受管链接、环境变量、退出项、目录名),只改名字;
|
|
616
|
+
目录名保持不变,所以目录、登录与共享链接都不动。旧启动命令由收敛计划按“账号移除”删除,
|
|
617
|
+
accounts.plan 会因目录仍被新名字使用而跳过删除共享链接。
|
|
618
|
+
"""
|
|
619
|
+
old_name = _checked_name(args.old)
|
|
620
|
+
new_name = _checked_name(args.new)
|
|
621
|
+
account = new.find(old_name)
|
|
622
|
+
if account is None:
|
|
623
|
+
error(new.not_registered(old_name), phase="rename")
|
|
624
|
+
return accounts.EXIT_ERROR
|
|
625
|
+
if new_name.casefold() == account.name.casefold():
|
|
626
|
+
# 大小写不敏感的文件系统上 codex-Work 与 codex-work 是同一个文件,删旧建新会互相覆盖。
|
|
627
|
+
raise UsageError("only the letter case differs; renaming that way is not supported")
|
|
628
|
+
if new.find(new_name) is not None:
|
|
629
|
+
error("account {!r} already exists".format(new.find(new_name).name), phase="rename")
|
|
630
|
+
return accounts.EXIT_CONFLICT
|
|
631
|
+
renamed = account.copy()
|
|
632
|
+
renamed.name = new_name
|
|
633
|
+
# 按原顺序重建字典,config.json 里账号的顺序不变。
|
|
634
|
+
new.accounts = {(new_name if key == account.name else key): (renamed if key == account.name else value)
|
|
635
|
+
for key, value in new.accounts.items()}
|
|
636
|
+
for path, bound in list(new.bindings.items()):
|
|
637
|
+
if bound.casefold() == account.name.casefold():
|
|
638
|
+
new.bindings[path] = new_name
|
|
639
|
+
return new, frozenset([account.name.casefold()])
|
|
549
640
|
|
|
550
641
|
|
|
551
642
|
def _not_registered_for_set(config: Config, name: str) -> str:
|
|
@@ -749,10 +840,19 @@ def _load_apply_file(path: str, old: Config) -> Config:
|
|
|
749
840
|
except OSError as exc:
|
|
750
841
|
raise ConfigError("cannot read {}: {}".format(path, exc))
|
|
751
842
|
new = parse_config(raw, path)
|
|
843
|
+
# parse_config 会给省略的 dir 填默认值,要判断文件里是否显式写了,只能看原始 JSON。
|
|
844
|
+
raw_accounts = json.loads(raw).get("accounts", {})
|
|
752
845
|
for account in new.accounts.values():
|
|
753
846
|
_warn_if_socks(account.proxy, account.name)
|
|
754
847
|
current = old.find(account.name)
|
|
755
848
|
account.managed_links = list(current.managed_links) if current else []
|
|
849
|
+
if current is not None:
|
|
850
|
+
# 目录名是工具内部状态:改了已登记账号的目录名,就等于让它指向另一个目录。
|
|
851
|
+
written = raw_accounts.get(account.name, {})
|
|
852
|
+
if isinstance(written, dict) and "dir" in written and written["dir"] != current.dir_name:
|
|
853
|
+
warn("'dir' of {!r} in {} is ignored for a registered account; it keeps using {}".format(
|
|
854
|
+
account.name, path, display_path(accounts.account_dir(old, current.name))))
|
|
855
|
+
account.dir_name = current.dir_name
|
|
756
856
|
# 绑定是本机状态,沿用当前值;文件中已删除的账号,它的绑定一并删除。
|
|
757
857
|
new.bindings = dict(old.bindings)
|
|
758
858
|
for name in set(new.bindings.values()):
|
|
@@ -829,6 +929,7 @@ def cmd_list(as_json: bool = False, verbose: bool = False) -> int:
|
|
|
829
929
|
"dir_status": "ok" if is_dir else "missing-dir",
|
|
830
930
|
"proxy": account.proxy,
|
|
831
931
|
"shared": account.shared,
|
|
932
|
+
"shared_exclude": list(account.shared_exclude),
|
|
832
933
|
"launcher": accounts.launcher_status(config, name),
|
|
833
934
|
"credentials_store": found.store if found else None,
|
|
834
935
|
# 只给键名:值里可能有密钥。
|
|
@@ -856,7 +957,7 @@ def cmd_list(as_json: bool = False, verbose: bool = False) -> int:
|
|
|
856
957
|
rows = [("NAME", "DIR", "PROXY", "SHARED", "LAUNCHER", "LOGIN", "PLAN")]
|
|
857
958
|
for name, account, directory, is_dir, found in entries:
|
|
858
959
|
rows.append((name, "ok" if is_dir else "missing-dir", account.proxy,
|
|
859
|
-
|
|
960
|
+
_shared_cell(account), accounts.launcher_status(config, name),
|
|
860
961
|
identity.display_login(found) if found else "-",
|
|
861
962
|
(found.plan if found and found.plan else "-")))
|
|
862
963
|
widths = [max(len(row[index]) for row in rows) for index in range(len(rows[0]))]
|
|
@@ -899,7 +1000,7 @@ def _print_list_summary(config: Config, entries, duplicates: List[List[str]]) ->
|
|
|
899
1000
|
problems.append("not logged in")
|
|
900
1001
|
has_problem = has_problem or bool(problems)
|
|
901
1002
|
rows.append((name, identity.display_login(found) if found else "-", account.proxy,
|
|
902
|
-
|
|
1003
|
+
_shared_cell(account), cell, ", ".join(problems) or "ok"))
|
|
903
1004
|
widths = [max(len(row[index]) for row in rows) for index in range(len(rows[0]))]
|
|
904
1005
|
for row in rows:
|
|
905
1006
|
print(" ".join(cell.ljust(width) for cell, width in zip(row, widths)).rstrip())
|
|
@@ -911,6 +1012,15 @@ def _print_list_summary(config: Config, entries, duplicates: List[List[str]]) ->
|
|
|
911
1012
|
return accounts.EXIT_OK
|
|
912
1013
|
|
|
913
1014
|
|
|
1015
|
+
def _shared_cell(account: Account) -> str:
|
|
1016
|
+
"""SHARED 列:共享开启且有退出项时写出退出的项;没有退出项时与 0.8 相同(yes / no)。"""
|
|
1017
|
+
if not account.shared:
|
|
1018
|
+
return "no"
|
|
1019
|
+
if account.shared_exclude:
|
|
1020
|
+
return "yes (not: {})".format(", ".join(account.shared_exclude))
|
|
1021
|
+
return "yes"
|
|
1022
|
+
|
|
1023
|
+
|
|
914
1024
|
def _usage_cell(result: usage.UsageResult, now: float) -> str:
|
|
915
1025
|
"""简表的 USAGE 单元格,如 `5h 23%, 7d 41%`;没有快照时为 `-`。
|
|
916
1026
|
|
|
@@ -1074,7 +1184,7 @@ def cmd_code(args: argparse.Namespace, extra: List[str]) -> int:
|
|
|
1074
1184
|
error("VS Code's `code` command was not found{}; install it from VS Code (\"Shell Command: Install "
|
|
1075
1185
|
"'code' command in PATH\") or pass --bin".format(" at " + args.bin if args.bin else ""))
|
|
1076
1186
|
return accounts.EXIT_ERROR
|
|
1077
|
-
data_dir = apps.gui_data_dir(config, account.
|
|
1187
|
+
data_dir = apps.gui_data_dir(config, account.dir_name, "vscode")
|
|
1078
1188
|
if sys.platform == "darwin":
|
|
1079
1189
|
if len(data_dir) > apps.SOCKET_DIR_WARN_LENGTH:
|
|
1080
1190
|
warn("{} is long; VS Code's socket path inside it may exceed the 104-byte limit".format(data_dir))
|
|
@@ -1107,8 +1217,8 @@ def cmd_app(args: argparse.Namespace) -> int:
|
|
|
1107
1217
|
if problem:
|
|
1108
1218
|
error(problem)
|
|
1109
1219
|
return accounts.EXIT_ERROR
|
|
1110
|
-
data_dir = apps.gui_data_dir(config, account.
|
|
1111
|
-
log = apps.desktop_log(config, account.
|
|
1220
|
+
data_dir = apps.gui_data_dir(config, account.dir_name, "desktop")
|
|
1221
|
+
log = apps.desktop_log(config, account.dir_name)
|
|
1112
1222
|
argv = [apps.open_command(), "-n",
|
|
1113
1223
|
"--env", "CODEX_HOME=" + accounts.account_dir(config, account.name),
|
|
1114
1224
|
"--env", "CODEX_ELECTRON_USER_DATA_PATH=" + data_dir,
|
|
@@ -13,8 +13,8 @@ import argparse
|
|
|
13
13
|
from typing import Dict, List, Tuple
|
|
14
14
|
|
|
15
15
|
# 第一个位置参数是账号名的子命令;usage 的每个位置参数都是账号名。
|
|
16
|
-
ACCOUNT_COMMANDS = ("add", "set", "login", "proxy", "remove", "migrate-default", "run", "path", "env",
|
|
17
|
-
"restore", "bind", "code", "app")
|
|
16
|
+
ACCOUNT_COMMANDS = ("add", "set", "rename", "login", "proxy", "remove", "migrate-default", "run", "path", "env",
|
|
17
|
+
"usage", "use", "restore", "bind", "code", "app")
|
|
18
18
|
MULTI_ACCOUNT_COMMANDS = ("usage",)
|
|
19
19
|
SHELLS = ("bash", "zsh", "fish")
|
|
20
20
|
|