codex-workspaces 0.2.0__tar.gz → 0.3.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 (27) hide show
  1. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/CHANGELOG.md +2 -1
  2. {codex_workspaces-0.2.0/src/codex_workspaces.egg-info → codex_workspaces-0.3.0}/PKG-INFO +11 -1
  3. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/README.MD +10 -0
  4. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/README.zh-CN.md +10 -0
  5. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/docs/RELEASE.zh-CN.md +2 -2
  6. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/docs/TESTING.zh-CN.md +1 -1
  7. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/pyproject.toml +1 -1
  8. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/__init__.py +1 -1
  9. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/cli.py +24 -0
  10. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/core.py +122 -0
  11. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0/src/codex_workspaces.egg-info}/PKG-INFO +11 -1
  12. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/tests/test_cli.py +14 -0
  13. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/tests/test_core.py +81 -3
  14. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/MANIFEST.in +0 -0
  15. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/docs/DESIGN.zh-CN.md +0 -0
  16. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/setup.cfg +0 -0
  17. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/__main__.py +0 -0
  18. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/config.py +0 -0
  19. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/errors.py +0 -0
  20. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/platforms.py +0 -0
  21. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/stats.py +0 -0
  22. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces/store.py +0 -0
  23. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces.egg-info/SOURCES.txt +0 -0
  24. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces.egg-info/dependency_links.txt +0 -0
  25. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces.egg-info/entry_points.txt +0 -0
  26. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces.egg-info/requires.txt +0 -0
  27. {codex_workspaces-0.2.0 → codex_workspaces-0.3.0}/src/codex_workspaces.egg-info/top_level.txt +0 -0
@@ -4,7 +4,7 @@ All notable changes to `codex-workspaces` will be documented in this file.
4
4
 
5
5
  This project follows a simple changelog format while it is still pre-release.
6
6
 
7
- ## Unreleased
7
+ ## 0.3.0 - 2026-07-04
8
8
 
9
9
  ### Added
10
10
 
@@ -18,6 +18,7 @@ This project follows a simple changelog format while it is still pre-release.
18
18
  - Added the unified `~/.codex-workspaces/` root with workspace metadata, account snapshots, and default-account restore behavior.
19
19
  - Added legacy workspace migration with `migrate`, `migrate --dry-run`, and `init <workspace> --migrate-current`.
20
20
  - Added legacy account import with `accounts import-legacy` and workspace auth import with `accounts import-workspaces`.
21
+ - Added account `rename`, guarded `delete --force`, and account `note` management.
21
22
  - Added `pyproject.toml`, package metadata, editable install support, and PyPI-ready build configuration.
22
23
  - Added GitHub Actions CI for Linux, macOS, Windows, and Python 3.9/3.11/3.13.
23
24
  - Added GitHub Actions Trusted Publishing workflow for PyPI releases.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-workspaces
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Cross-platform Codex workspace switcher with a preserved macOS shell workflow.
5
5
  Author: blockchain-project-lives
6
6
  Project-URL: Homepage, https://github.com/blockchain-project-lives/codex-workspaces
@@ -190,6 +190,16 @@ codex-workspaces accounts restore-default
190
190
 
191
191
  `auth.json` contains credentials. Do not commit workspace directories, account snapshots, SQLite state, sessions, or shell snapshots to git; the project `.gitignore` excludes these patterns for local checkouts.
192
192
 
193
+ Manage account metadata and snapshots:
194
+
195
+ ```bash
196
+ codex-workspaces accounts note acct_research "lab account"
197
+ codex-workspaces accounts rename acct_research acct_lab
198
+ codex-workspaces accounts delete acct_lab --force
199
+ ```
200
+
201
+ Account deletion always requires `--force` and refuses to delete an account that is still configured as any workspace's default account.
202
+
193
203
  Manage workspace metadata and lifecycle:
194
204
 
195
205
  ```bash
@@ -162,6 +162,16 @@ codex-workspaces accounts restore-default
162
162
 
163
163
  `auth.json` contains credentials. Do not commit workspace directories, account snapshots, SQLite state, sessions, or shell snapshots to git; the project `.gitignore` excludes these patterns for local checkouts.
164
164
 
165
+ Manage account metadata and snapshots:
166
+
167
+ ```bash
168
+ codex-workspaces accounts note acct_research "lab account"
169
+ codex-workspaces accounts rename acct_research acct_lab
170
+ codex-workspaces accounts delete acct_lab --force
171
+ ```
172
+
173
+ Account deletion always requires `--force` and refuses to delete an account that is still configured as any workspace's default account.
174
+
165
175
  Manage workspace metadata and lifecycle:
166
176
 
167
177
  ```bash
@@ -162,6 +162,16 @@ codex-workspaces accounts restore-default
162
162
 
163
163
  `auth.json` 包含认证凭据,不要提交到 git。工作区目录、账号快照、SQLite 状态、sessions 和 shell snapshots 已在本项目 `.gitignore` 中排除。
164
164
 
165
+ 管理账号备注和快照生命周期:
166
+
167
+ ```bash
168
+ codex-workspaces accounts note acct_research "实验室账号"
169
+ codex-workspaces accounts rename acct_research acct_lab
170
+ codex-workspaces accounts delete acct_lab --force
171
+ ```
172
+
173
+ 删除账号始终需要 `--force`;如果某个工作区仍把该账号设为默认账号,删除会被拒绝。
174
+
165
175
  管理工作区备注和生命周期:
166
176
 
167
177
  ```bash
@@ -88,9 +88,9 @@ permissions:
88
88
  1. 更新版本号和 `CHANGELOG.md`。
89
89
  2. 本地执行测试和构建检查。
90
90
  3. 合并到 `main`。
91
- 4. 创建 Git tag,例如 `v0.2.0`,触发 `Publish to TestPyPI`。
91
+ 4. 创建 Git tag,例如 `v0.3.0`,触发 `Publish to TestPyPI`。
92
92
  5. 确认 TestPyPI 上传和安装正常。
93
- 6. 从同一个提交创建并推送正式发布分支,例如 `release/v0.2.0`,触发 `Publish to PyPI`。
93
+ 6. 从同一个提交创建并推送正式发布分支,例如 `release/v0.3.0`,触发 `Publish to PyPI`。
94
94
  7. 如果 `pypi` Environment 配置了 Required reviewers,在 GitHub Actions 里批准部署。
95
95
  8. 在 PyPI 页面确认 wheel、sdist 和 README 渲染正常。
96
96
 
@@ -7,7 +7,7 @@
7
7
  - 文件系统行为:初始化统一目录工作区、切换链接、拒绝覆盖真实目录。
8
8
  - CLI 行为:命令别名、工作区名快捷切换、错误路径、帮助输出。
9
9
  - 管理行为:诊断、列表元信息、重命名、删除保护、备注读写和账号绑定。
10
- - 账号行为:账号快照保存、列表、临时切换、默认账号恢复、默认账号设置。
10
+ - 账号行为:账号快照保存、列表、备注、重命名、删除保护、临时切换、默认账号恢复、默认账号设置。
11
11
  - 迁移行为:旧 `~/.codex-<name>` 工作区迁移、旧 `~/.codex-accounts` 导入、dry-run 不落盘、迁移前备份。
12
12
  - 统计行为:只读 `state_*.sqlite`,汇总 token、模型、最近会话和每日用量。
13
13
  - 平台行为:macOS App 控制可注入,非 macOS 自动跳过 App 启停,Codex 内置 Terminal 阻止或转交危险操作。
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-workspaces"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Cross-platform Codex workspace switcher with a preserved macOS shell workflow."
9
9
  readme = "README.MD"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """Codex workspace switching utilities."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.3.0"
@@ -231,6 +231,30 @@ def run_accounts(args: Sequence[str], manager: WorkspaceManager) -> int:
231
231
  )
232
232
  manager.accounts_set_default(positional[0], positional[1], activate)
233
233
  return 0
234
+ if command in {"rename", "mv"}:
235
+ if len(rest) != 2:
236
+ manager.fail(
237
+ "用法: codex-workspaces accounts rename <旧账号> <新账号>",
238
+ "Usage: codex-workspaces accounts rename <old-account> <new-account>",
239
+ )
240
+ manager.accounts_rename(rest[0], rest[1])
241
+ return 0
242
+ if command in {"delete", "remove", "rm"}:
243
+ if not rest:
244
+ manager.fail(
245
+ "用法: codex-workspaces accounts delete <账号> --force",
246
+ "Usage: codex-workspaces accounts delete <account> --force",
247
+ )
248
+ manager.accounts_delete(rest[0], rest[1:])
249
+ return 0
250
+ if command == "note":
251
+ if not rest:
252
+ manager.fail(
253
+ "用法: codex-workspaces accounts note <账号> [备注文本|--clear]",
254
+ "Usage: codex-workspaces accounts note <account> [note text|--clear]",
255
+ )
256
+ manager.accounts_note(rest[0], rest[1:])
257
+ return 0
234
258
  if command == "import-workspaces":
235
259
  if rest:
236
260
  manager.fail(f"未知参数: {rest[0]}", f"Unknown option: {rest[0]}")
@@ -1139,6 +1139,122 @@ class WorkspaceManager:
1139
1139
  self.store.write_account_meta(account_meta)
1140
1140
  self.info(self.message(f"已设置默认账号: {clean_name} -> {account_id}", f"Set default account: {clean_name} -> {account_id}"))
1141
1141
 
1142
+ def workspace_account_references(self, account_id: str) -> tuple[list[str], list[str]]:
1143
+ default_refs: list[str] = []
1144
+ active_refs: list[str] = []
1145
+ for directory in self.workspace_dirs():
1146
+ name = strip_workspace_name(str(directory))
1147
+ meta = self.store.ensure_workspace_meta(name, directory)
1148
+ if meta.default_account_id == account_id:
1149
+ default_refs.append(name)
1150
+ if meta.active_account_id == account_id:
1151
+ active_refs.append(name)
1152
+ return default_refs, active_refs
1153
+
1154
+ def accounts_rename(self, old_account: str, new_account: str) -> None:
1155
+ self.store.ensure_layout()
1156
+ old_id = self.account_id_from_input(old_account)
1157
+ new_id = self.account_id_from_input(new_account)
1158
+ if old_id == new_id:
1159
+ self.fail("新旧账号名相同。", "Old and new account names are the same.")
1160
+ old_dir = self.store.account_dir(old_id)
1161
+ new_dir = self.store.account_dir(new_id)
1162
+ if not self.store.account_meta_path(old_id).is_file():
1163
+ self.fail(f"账号不存在: {old_id}", f"Account not found: {old_id}\nHint: run `codex-workspaces accounts list`")
1164
+ if new_dir.exists():
1165
+ self.fail(f"目标账号已存在: {new_id}", f"Target account already exists: {new_id}")
1166
+
1167
+ with self.store.lock():
1168
+ old_dir.rename(new_dir)
1169
+ meta = self.store.read_account_meta(new_id)
1170
+ meta.id = new_id
1171
+ meta.name = self.account_name_from_input(new_account)
1172
+ meta.updated_at = iso_now()
1173
+ self.store.write_account_meta(meta)
1174
+ old_meta_path = self.store.account_meta_path(old_id)
1175
+ if old_meta_path.exists():
1176
+ old_meta_path.unlink()
1177
+
1178
+ for directory in self.workspace_dirs():
1179
+ name = strip_workspace_name(str(directory))
1180
+ workspace_meta = self.store.ensure_workspace_meta(name, directory)
1181
+ changed = False
1182
+ if workspace_meta.default_account_id == old_id:
1183
+ workspace_meta.default_account_id = new_id
1184
+ changed = True
1185
+ if workspace_meta.active_account_id == old_id:
1186
+ workspace_meta.active_account_id = new_id
1187
+ changed = True
1188
+ if changed:
1189
+ workspace_meta.updated_at = iso_now()
1190
+ self.store.write_workspace_meta(directory, workspace_meta)
1191
+ self.info(self.message(f"已重命名账号: {old_id} -> {new_id}", f"Renamed account: {old_id} -> {new_id}"))
1192
+
1193
+ def accounts_delete(self, account: str, args: Sequence[str]) -> None:
1194
+ self.store.ensure_layout()
1195
+ force = False
1196
+ for arg in args:
1197
+ if arg == "--force":
1198
+ force = True
1199
+ else:
1200
+ self.fail(f"未知参数: {arg}", f"Unknown option: {arg}")
1201
+ if not force:
1202
+ self.fail(
1203
+ "删除账号需要 --force,避免误删认证快照。",
1204
+ "Deleting an account requires --force to avoid accidental credential loss.",
1205
+ )
1206
+
1207
+ account_id = self.account_id_from_input(account)
1208
+ account_dir = self.store.account_dir(account_id)
1209
+ if not self.store.account_meta_path(account_id).is_file():
1210
+ self.fail(f"账号不存在: {account_id}", f"Account not found: {account_id}\nHint: run `codex-workspaces accounts list`")
1211
+
1212
+ default_refs, active_refs = self.workspace_account_references(account_id)
1213
+ if default_refs:
1214
+ refs = ", ".join(default_refs)
1215
+ self.fail(
1216
+ f"不能删除默认账号 {account_id},仍被工作区使用: {refs}",
1217
+ f"Cannot delete default account {account_id}; still used by workspaces: {refs}\nHint: run `codex-workspaces accounts set-default <workspace> <account>` first.",
1218
+ )
1219
+
1220
+ with self.store.lock():
1221
+ shutil.rmtree(account_dir)
1222
+ for directory in self.workspace_dirs():
1223
+ name = strip_workspace_name(str(directory))
1224
+ workspace_meta = self.store.ensure_workspace_meta(name, directory)
1225
+ if workspace_meta.active_account_id == account_id:
1226
+ workspace_meta.active_account_id = None
1227
+ workspace_meta.updated_at = iso_now()
1228
+ self.store.write_workspace_meta(directory, workspace_meta)
1229
+ suffix = f" ({', '.join(active_refs)})" if active_refs else ""
1230
+ self.info(self.message(f"已删除账号: {account_id}{suffix}", f"Deleted account: {account_id}{suffix}"))
1231
+
1232
+ def accounts_note(self, account: str, args: Sequence[str]) -> None:
1233
+ self.store.ensure_layout()
1234
+ account_id = self.account_id_from_input(account)
1235
+ if not self.store.account_meta_path(account_id).is_file():
1236
+ self.fail(f"账号不存在: {account_id}", f"Account not found: {account_id}\nHint: run `codex-workspaces accounts list`")
1237
+ meta = self.store.read_account_meta(account_id)
1238
+ if not args:
1239
+ if meta.notes:
1240
+ self.info(meta.notes)
1241
+ else:
1242
+ self.info(self.message("未设置备注。", "No note set."))
1243
+ return
1244
+ if len(args) == 1 and args[0] == "--clear":
1245
+ meta.notes = ""
1246
+ meta.updated_at = iso_now()
1247
+ self.store.write_account_meta(meta)
1248
+ self.info(self.message(f"已清除账号备注: {account_id}", f"Cleared account note: {account_id}"))
1249
+ return
1250
+ text = " ".join(args).strip()
1251
+ if not text:
1252
+ self.fail("备注不能为空。", "Note cannot be empty.")
1253
+ meta.notes = text
1254
+ meta.updated_at = iso_now()
1255
+ self.store.write_account_meta(meta)
1256
+ self.info(self.message(f"已更新账号备注: {account_id}", f"Updated account note: {account_id}"))
1257
+
1142
1258
  def accounts_import_workspaces(self) -> None:
1143
1259
  self.store.ensure_layout()
1144
1260
  imported: list[str] = []
@@ -1333,6 +1449,9 @@ def usage(lang: str) -> str:
1333
1449
  codex-workspaces accounts use <账号>
1334
1450
  codex-workspaces accounts restore-default [工作区]
1335
1451
  codex-workspaces accounts set-default <工作区> <账号> [--activate]
1452
+ codex-workspaces accounts rename <旧账号> <新账号>
1453
+ codex-workspaces accounts delete <账号> --force
1454
+ codex-workspaces accounts note <账号> [备注文本|--clear]
1336
1455
  codex-workspaces accounts import-workspaces
1337
1456
  codex-workspaces accounts import-legacy <旧账号目录>
1338
1457
  管理 auth.json 账号快照。accounts use 是临时切换,不修改工作区默认账号。
@@ -1411,6 +1530,9 @@ Usage:
1411
1530
  codex-workspaces accounts use <account>
1412
1531
  codex-workspaces accounts restore-default [workspace]
1413
1532
  codex-workspaces accounts set-default <workspace> <account> [--activate]
1533
+ codex-workspaces accounts rename <old-account> <new-account>
1534
+ codex-workspaces accounts delete <account> --force
1535
+ codex-workspaces accounts note <account> [note text|--clear]
1414
1536
  codex-workspaces accounts import-workspaces
1415
1537
  codex-workspaces accounts import-legacy <legacy-accounts-dir>
1416
1538
  Manage auth.json account snapshots. accounts use is temporary and does not change the workspace default account.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-workspaces
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Cross-platform Codex workspace switcher with a preserved macOS shell workflow.
5
5
  Author: blockchain-project-lives
6
6
  Project-URL: Homepage, https://github.com/blockchain-project-lives/codex-workspaces
@@ -190,6 +190,16 @@ codex-workspaces accounts restore-default
190
190
 
191
191
  `auth.json` contains credentials. Do not commit workspace directories, account snapshots, SQLite state, sessions, or shell snapshots to git; the project `.gitignore` excludes these patterns for local checkouts.
192
192
 
193
+ Manage account metadata and snapshots:
194
+
195
+ ```bash
196
+ codex-workspaces accounts note acct_research "lab account"
197
+ codex-workspaces accounts rename acct_research acct_lab
198
+ codex-workspaces accounts delete acct_lab --force
199
+ ```
200
+
201
+ Account deletion always requires `--force` and refuses to delete an account that is still configured as any workspace's default account.
202
+
193
203
  Manage workspace metadata and lifecycle:
194
204
 
195
205
  ```bash
@@ -114,6 +114,20 @@ class TestCliDispatch:
114
114
  assert "acct_work" in output
115
115
  assert "active=acct_work default=acct_work" in output
116
116
 
117
+ def test_account_lifecycle_dispatches(self, tmp_path: Path) -> None:
118
+ manager = manager_for(tmp_path)
119
+
120
+ assert run(["accounts", "init", "research"], manager) == 0
121
+ assert run(["accounts", "note", "research", "lab"], manager) == 0
122
+ assert run(["accounts", "rename", "research", "lab"], manager) == 0
123
+ assert run(["accounts", "delete", "lab", "--force"], manager) == 0
124
+
125
+ output = manager.stdout.getvalue()
126
+ assert "Initialized account: acct_research" in output
127
+ assert "Updated account note: acct_research" in output
128
+ assert "Renamed account: acct_research -> acct_lab" in output
129
+ assert "Deleted account: acct_lab" in output
130
+
117
131
  def test_migrate_and_import_legacy_dispatch(self, tmp_path: Path) -> None:
118
132
  manager = manager_for(tmp_path)
119
133
  legacy = manager.config.home_dir / ".codex-work"
@@ -147,7 +147,7 @@ class TestWorkspaceNames:
147
147
 
148
148
 
149
149
  class TestSystemPlatform:
150
- def test_force_stop_waits_five_seconds_before_killall(self, monkeypatch) -> None:
150
+ def test_force_stop_waits_short_grace_period_before_killall(self, monkeypatch) -> None:
151
151
  platform = StubbornMacPlatform()
152
152
  stdout = io.StringIO()
153
153
  calls = []
@@ -164,8 +164,8 @@ class TestSystemPlatform:
164
164
 
165
165
  assert ["osascript", "-e", 'tell application "Codex" to quit'] in calls
166
166
  assert ["killall", "Codex"] in calls
167
- assert sleeps == [1, 1, 1, 1, 1, 1]
168
- assert "did not exit within 5s" in stdout.getvalue()
167
+ assert sleeps == [1] * (platforms_module.FORCE_QUIT_GRACE_SECONDS + 1)
168
+ assert f"did not exit within {platforms_module.FORCE_QUIT_GRACE_SECONDS}s" in stdout.getvalue()
169
169
 
170
170
 
171
171
  class TestWorkspaceManager:
@@ -565,3 +565,81 @@ class TestWorkspaceManager:
565
565
  assert meta.active_account_id == "acct_work"
566
566
  assert manager.store.account_auth_path("acct_work").read_text(encoding="utf-8") == '{"account":"work"}\n'
567
567
  assert "Imported workspace default accounts: 1" in stdout.getvalue()
568
+
569
+ def test_accounts_note_sets_reads_and_clears_meta_notes(self, tmp_path: Path) -> None:
570
+ manager, stdout, _ = make_manager(tmp_path)
571
+ manager.accounts_init("research")
572
+
573
+ manager.accounts_note("research", ["lab", "account"])
574
+ manager.accounts_note("research", [])
575
+ manager.accounts_note("research", ["--clear"])
576
+ manager.accounts_note("research", [])
577
+
578
+ meta = manager.store.read_account_meta("acct_research")
579
+ output = stdout.getvalue()
580
+ assert meta.notes == ""
581
+ assert "Updated account note: acct_research" in output
582
+ assert "lab account" in output
583
+ assert "Cleared account note: acct_research" in output
584
+ assert "No note set." in output
585
+
586
+ def test_accounts_rename_updates_workspace_default_and_active_refs(self, tmp_path: Path) -> None:
587
+ manager, stdout, _ = make_manager(tmp_path)
588
+ manager.init_workspace("work", [])
589
+ manager.switch_workspace("work", ["--no-stop", "--no-start"], ["switch", "work"])
590
+ (manager.workspace_dir("work") / "auth.json").write_text('{"account":"work"}\n', encoding="utf-8")
591
+ manager.accounts_save("work")
592
+ manager.accounts_set_default("work", "work", activate=True)
593
+
594
+ manager.accounts_rename("work", "office")
595
+
596
+ meta = manager.store.read_workspace_meta(manager.workspace_dir("work"), "work")
597
+ account_meta = manager.store.read_account_meta("acct_office")
598
+ assert meta.default_account_id == "acct_office"
599
+ assert meta.active_account_id == "acct_office"
600
+ assert account_meta.id == "acct_office"
601
+ assert account_meta.name == "office"
602
+ assert not manager.store.account_dir("acct_work").exists()
603
+ assert manager.store.account_auth_path("acct_office").read_text(encoding="utf-8") == '{"account":"work"}\n'
604
+ assert "Renamed account: acct_work -> acct_office" in stdout.getvalue()
605
+
606
+ def test_accounts_delete_requires_force_and_refuses_default_account(self, tmp_path: Path) -> None:
607
+ manager, _, _ = make_manager(tmp_path)
608
+ manager.init_workspace("work", [])
609
+ manager.switch_workspace("work", ["--no-stop", "--no-start"], ["switch", "work"])
610
+ (manager.workspace_dir("work") / "auth.json").write_text('{"account":"work"}\n', encoding="utf-8")
611
+ manager.accounts_save("work")
612
+ manager.accounts_set_default("work", "work", activate=True)
613
+
614
+ with pytest.raises(CodexWorkspacesError, match="requires --force"):
615
+ manager.accounts_delete("work", [])
616
+ with pytest.raises(CodexWorkspacesError, match="Cannot delete default account"):
617
+ manager.accounts_delete("work", ["--force"])
618
+
619
+ assert manager.store.account_dir("acct_work").is_dir()
620
+
621
+ def test_accounts_delete_removes_non_default_and_clears_active_refs(self, tmp_path: Path) -> None:
622
+ manager, stdout, _ = make_manager(tmp_path)
623
+ manager.init_workspace("work", [])
624
+ manager.switch_workspace("work", ["--no-stop", "--no-start"], ["switch", "work"])
625
+ (manager.workspace_dir("work") / "auth.json").write_text('{"account":"work"}\n', encoding="utf-8")
626
+ manager.accounts_save("work")
627
+ manager.accounts_set_default("work", "work", activate=True)
628
+ personal_auth = tmp_path / "personal-auth.json"
629
+ personal_auth.write_text('{"account":"personal"}\n', encoding="utf-8")
630
+ manager.store.create_account(
631
+ "acct_personal",
632
+ name="personal",
633
+ source="manual",
634
+ bound_workspace=None,
635
+ auth_source=personal_auth,
636
+ )
637
+ manager.accounts_use("personal")
638
+
639
+ manager.accounts_delete("personal", ["--force"])
640
+
641
+ meta = manager.store.read_workspace_meta(manager.workspace_dir("work"), "work")
642
+ assert meta.default_account_id == "acct_work"
643
+ assert meta.active_account_id is None
644
+ assert not manager.store.account_dir("acct_personal").exists()
645
+ assert "Deleted account: acct_personal" in stdout.getvalue()