codex-manager 1.0.1__tar.gz → 3.0.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.
- codex_manager-3.0.0/CODEX_MANAGER_SPEC.md +49 -0
- codex_manager-3.0.0/PKG-INFO +83 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/pyproject.toml +2 -1
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/__init__.py +1 -1
- codex_manager-3.0.0/src/codex_manager/account_status.py +289 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/args.py +295 -87
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/backup.py +99 -25
- codex_manager-3.0.0/src/codex_manager/cli.py +434 -0
- codex_manager-3.0.0/src/codex_manager/cloud.py +67 -0
- codex_manager-3.0.0/src/codex_manager/config.py +34 -0
- codex_manager-3.0.0/src/codex_manager/cooldown.py +217 -0
- codex_manager-3.0.0/src/codex_manager/credentials.py +90 -0
- codex_manager-3.0.0/src/codex_manager/doctor.py +112 -0
- codex_manager-3.0.0/src/codex_manager/list_backups.py +264 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/profile.py +2 -1
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/prune.py +0 -2
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/prune_backups.py +9 -10
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/recommend.py +4 -4
- codex_manager-3.0.0/src/codex_manager/registry.py +113 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager/restore.py +65 -27
- codex_manager-3.0.0/src/codex_manager/status.py +262 -0
- codex_manager-3.0.0/src/codex_manager/sync.py +99 -0
- codex_manager-3.0.0/src/codex_manager/ui.py +134 -0
- codex_manager-3.0.0/src/codex_manager/use_account.py +79 -0
- codex_manager-3.0.0/src/codex_manager/utils.py +13 -0
- codex_manager-3.0.0/src/codex_manager.egg-info/PKG-INFO +83 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager.egg-info/SOURCES.txt +28 -6
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager.egg-info/requires.txt +1 -0
- codex_manager-3.0.0/tests/test_args_cli.py +139 -0
- codex_manager-3.0.0/tests/test_args_cli_3.py +15 -0
- codex_manager-3.0.0/tests/test_args_cli_extra.py +38 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_backup.py +29 -1
- codex_manager-3.0.0/tests/test_backup2.py +76 -0
- codex_manager-3.0.0/tests/test_cli2.py +83 -0
- codex_manager-3.0.0/tests/test_cloud.py +75 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_config.py +1 -1
- codex_manager-3.0.0/tests/test_cooldown.py +87 -0
- codex_manager-3.0.0/tests/test_cooldown2.py +57 -0
- codex_manager-3.0.0/tests/test_credentials2.py +75 -0
- codex_manager-3.0.0/tests/test_doctor.py +81 -0
- codex_manager-3.0.0/tests/test_doctor2.py +53 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_list_backups.py +1 -1
- codex_manager-3.0.0/tests/test_list_backups2.py +61 -0
- codex_manager-3.0.0/tests/test_list_backups3.py +103 -0
- codex_manager-3.0.0/tests/test_next_gen_upgrade.py +103 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_prune_backups.py +1 -1
- codex_manager-3.0.0/tests/test_prune_backups2.py +52 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_recommend.py +29 -25
- codex_manager-3.0.0/tests/test_recommend2.py +23 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_restore.py +1 -2
- codex_manager-3.0.0/tests/test_restore2.py +76 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_status.py +1 -1
- codex_manager-3.0.0/tests/test_status2.py +105 -0
- codex_manager-3.0.0/tests/test_status3.py +19 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_sync.py +1 -4
- codex_manager-3.0.0/tests/test_sync2.py +77 -0
- codex_manager-3.0.0/tests/test_ui.py +63 -0
- codex_manager-3.0.0/tests/test_ui_2.py +35 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_use.py +2 -2
- codex_manager-3.0.0/tests/test_use2.py +48 -0
- codex_manager-1.0.1/CODEX_MANAGER_SPEC.md +0 -427
- codex_manager-1.0.1/PKG-INFO +0 -460
- codex_manager-1.0.1/README.md +0 -154
- codex_manager-1.0.1/src/codex_manager/cli.py +0 -243
- codex_manager-1.0.1/src/codex_manager/config.py +0 -50
- codex_manager-1.0.1/src/codex_manager/cooldown.py +0 -113
- codex_manager-1.0.1/src/codex_manager/doctor.py +0 -69
- codex_manager-1.0.1/src/codex_manager/inventory.py +0 -32
- codex_manager-1.0.1/src/codex_manager/list_backups.py +0 -129
- codex_manager-1.0.1/src/codex_manager/normalize.py +0 -119
- codex_manager-1.0.1/src/codex_manager/status.py +0 -180
- codex_manager-1.0.1/src/codex_manager/sync.py +0 -97
- codex_manager-1.0.1/src/codex_manager/use_account.py +0 -112
- codex_manager-1.0.1/src/codex_manager.egg-info/PKG-INFO +0 -460
- codex_manager-1.0.1/tests/test_cooldown.py +0 -85
- codex_manager-1.0.1/tests/test_doctor.py +0 -67
- codex_manager-1.0.1/tests/test_inventory.py +0 -30
- codex_manager-1.0.1/tests/test_normalize.py +0 -92
- {codex_manager-1.0.1 → codex_manager-3.0.0}/setup.cfg +0 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager.egg-info/dependency_links.txt +0 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager.egg-info/entry_points.txt +0 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/src/codex_manager.egg-info/top_level.txt +0 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_profile.py +0 -0
- {codex_manager-1.0.1 → codex_manager-3.0.0}/tests/test_prune.py +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Codex Manager
|
|
2
|
+
|
|
3
|
+
`codex-manager` is a CLI for backing up Codex account state and tracking account cooldown data from live `/status` output.
|
|
4
|
+
|
|
5
|
+
## Core model
|
|
6
|
+
|
|
7
|
+
- Backups are stored as `*.tar.gz` archives plus adjacent `*.metadata.json`.
|
|
8
|
+
- Metadata is the source of truth for `cooldown`, `recommend`, and account rotation decisions.
|
|
9
|
+
- Live Codex `/status` output is parsed to capture:
|
|
10
|
+
- account email
|
|
11
|
+
- quota text
|
|
12
|
+
- quota percent left when available
|
|
13
|
+
- weekly reset timestamp
|
|
14
|
+
|
|
15
|
+
## Main commands
|
|
16
|
+
|
|
17
|
+
- `cm backup`: capture live status, build archive metadata, and create a backup.
|
|
18
|
+
- `cm status`: parse live status and patch the latest metadata for the current account.
|
|
19
|
+
- `cm cooldown`: show account availability from stored metadata, optionally merged with live status.
|
|
20
|
+
- `cm recommend`: choose the best account to use next.
|
|
21
|
+
- `cm use`: switch to another account, defaulting to auth-only restore unless `--clean` is used.
|
|
22
|
+
- `cm restore`: restore a full backup into the Codex home.
|
|
23
|
+
|
|
24
|
+
## Status tracking policy
|
|
25
|
+
|
|
26
|
+
- `use` and `restore` sync the current account status before switching away from it.
|
|
27
|
+
- If live status capture fails, the command retries once.
|
|
28
|
+
- If status capture fails twice, the command exits and instructs the user to rerun with `--without-status-check`.
|
|
29
|
+
|
|
30
|
+
## Emergency fallback
|
|
31
|
+
|
|
32
|
+
`--without-status-check` exists for cases where Codex layout changes or live status is temporarily unavailable.
|
|
33
|
+
|
|
34
|
+
In that mode:
|
|
35
|
+
|
|
36
|
+
- cooldown is estimated as `now + 7 days`
|
|
37
|
+
- metadata is still written so cooldown state is not lost
|
|
38
|
+
- fallback archive and metadata names are based on the estimated reset time, not the current time
|
|
39
|
+
|
|
40
|
+
## Storage defaults
|
|
41
|
+
|
|
42
|
+
- manager home: `~/.codex-manager`
|
|
43
|
+
- backup directory: `~/.codex-manager/backups`
|
|
44
|
+
- Codex home: `~/.codex`
|
|
45
|
+
|
|
46
|
+
## Cloud support
|
|
47
|
+
|
|
48
|
+
- Backblaze B2 is supported for remote backup metadata and archive storage.
|
|
49
|
+
- Local and cloud entries can be merged for recommendation and cooldown reporting.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codex-manager
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: Codex account snapshot manager
|
|
5
|
+
Author-email: Dhruv <dhruv13x@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dhruv13x/codex-manager
|
|
8
|
+
Project-URL: Repository, https://github.com/dhruv13x/codex-manager
|
|
9
|
+
Project-URL: Issues, https://github.com/dhruv13x/codex-manager/issues
|
|
10
|
+
Keywords: codex,backup,cli,automation
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: rich>=13.0.0
|
|
19
|
+
Requires-Dist: boto3>=1.28.0
|
|
20
|
+
Requires-Dist: b2sdk>=2.0.0
|
|
21
|
+
Requires-Dist: relm>=7.1.1
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-timeout>=2.2.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-json-report>=1.5.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pyfakefs>=5.0.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
31
|
+
Requires-Dist: black>=24.3.0; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.11.0; extra == "dev"
|
|
33
|
+
Requires-Dist: types-PyYAML>=6.0; extra == "dev"
|
|
34
|
+
|
|
35
|
+
# Codex Manager
|
|
36
|
+
|
|
37
|
+
`codex-manager` is a CLI for backing up Codex account state and tracking account cooldown data from live `/status` output.
|
|
38
|
+
|
|
39
|
+
## Core model
|
|
40
|
+
|
|
41
|
+
- Backups are stored as `*.tar.gz` archives plus adjacent `*.metadata.json`.
|
|
42
|
+
- Metadata is the source of truth for `cooldown`, `recommend`, and account rotation decisions.
|
|
43
|
+
- Live Codex `/status` output is parsed to capture:
|
|
44
|
+
- account email
|
|
45
|
+
- quota text
|
|
46
|
+
- quota percent left when available
|
|
47
|
+
- weekly reset timestamp
|
|
48
|
+
|
|
49
|
+
## Main commands
|
|
50
|
+
|
|
51
|
+
- `cm backup`: capture live status, build archive metadata, and create a backup.
|
|
52
|
+
- `cm status`: parse live status and patch the latest metadata for the current account.
|
|
53
|
+
- `cm cooldown`: show account availability from stored metadata, optionally merged with live status.
|
|
54
|
+
- `cm recommend`: choose the best account to use next.
|
|
55
|
+
- `cm use`: switch to another account, defaulting to auth-only restore unless `--clean` is used.
|
|
56
|
+
- `cm restore`: restore a full backup into the Codex home.
|
|
57
|
+
|
|
58
|
+
## Status tracking policy
|
|
59
|
+
|
|
60
|
+
- `use` and `restore` sync the current account status before switching away from it.
|
|
61
|
+
- If live status capture fails, the command retries once.
|
|
62
|
+
- If status capture fails twice, the command exits and instructs the user to rerun with `--without-status-check`.
|
|
63
|
+
|
|
64
|
+
## Emergency fallback
|
|
65
|
+
|
|
66
|
+
`--without-status-check` exists for cases where Codex layout changes or live status is temporarily unavailable.
|
|
67
|
+
|
|
68
|
+
In that mode:
|
|
69
|
+
|
|
70
|
+
- cooldown is estimated as `now + 7 days`
|
|
71
|
+
- metadata is still written so cooldown state is not lost
|
|
72
|
+
- fallback archive and metadata names are based on the estimated reset time, not the current time
|
|
73
|
+
|
|
74
|
+
## Storage defaults
|
|
75
|
+
|
|
76
|
+
- manager home: `~/.codex-manager`
|
|
77
|
+
- backup directory: `~/.codex-manager/backups`
|
|
78
|
+
- Codex home: `~/.codex`
|
|
79
|
+
|
|
80
|
+
## Cloud support
|
|
81
|
+
|
|
82
|
+
- Backblaze B2 is supported for remote backup metadata and archive storage.
|
|
83
|
+
- Local and cloud entries can be merged for recommendation and cooldown reporting.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codex-manager"
|
|
3
|
-
version = "
|
|
3
|
+
version = "3.0.0"
|
|
4
4
|
description = "Codex account snapshot manager"
|
|
5
5
|
readme = "CODEX_MANAGER_SPEC.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -12,6 +12,7 @@ authors = [
|
|
|
12
12
|
dependencies = [
|
|
13
13
|
"rich>=13.0.0",
|
|
14
14
|
"boto3>=1.28.0",
|
|
15
|
+
"b2sdk>=2.0.0",
|
|
15
16
|
"relm>=7.1.1",
|
|
16
17
|
]
|
|
17
18
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .backup import read_status_text_from_args
|
|
11
|
+
from .cloud import get_cloud_provider
|
|
12
|
+
from .list_backups import list_backups, list_cloud_backups
|
|
13
|
+
from .registry import sync_registry_with_cloud, update_registry_entry
|
|
14
|
+
from .status import parse_live_status_text
|
|
15
|
+
from .ui import console
|
|
16
|
+
from .utils import build_archive_name
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def patch_metadata(
|
|
20
|
+
email: str,
|
|
21
|
+
reset_at: Any | None = None,
|
|
22
|
+
quota_text: str | None = None,
|
|
23
|
+
quota_percent_left: int | None = None,
|
|
24
|
+
args: Any = None,
|
|
25
|
+
session_start_at: Any | None = None,
|
|
26
|
+
is_expired: bool = False,
|
|
27
|
+
) -> None:
|
|
28
|
+
backup_dir = Path(args.backup_dir).expanduser() if args and hasattr(args, "backup_dir") else Path("~/.codex-manager/backups").expanduser()
|
|
29
|
+
|
|
30
|
+
# We will compute the final reset_at and session_start_at to save to registry
|
|
31
|
+
final_reset_at = reset_at
|
|
32
|
+
final_session_start_at = session_start_at
|
|
33
|
+
|
|
34
|
+
if backup_dir.exists():
|
|
35
|
+
# Find any metadata file containing this email
|
|
36
|
+
metadata_paths = []
|
|
37
|
+
for p in backup_dir.glob("*.metadata.json"):
|
|
38
|
+
if email in p.name:
|
|
39
|
+
metadata_paths.append(p)
|
|
40
|
+
|
|
41
|
+
if metadata_paths:
|
|
42
|
+
# Sort by name descending to get the latest
|
|
43
|
+
metadata_paths.sort(key=lambda p: p.name, reverse=True)
|
|
44
|
+
metadata_path = metadata_paths[0]
|
|
45
|
+
try:
|
|
46
|
+
data = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
47
|
+
|
|
48
|
+
if reset_at is not None:
|
|
49
|
+
data["reset_at"] = (
|
|
50
|
+
reset_at.isoformat() if hasattr(reset_at, "isoformat") else str(reset_at)
|
|
51
|
+
)
|
|
52
|
+
data["next_available_at"] = data["reset_at"]
|
|
53
|
+
|
|
54
|
+
if session_start_at is not None:
|
|
55
|
+
data["session_start_at"] = (
|
|
56
|
+
session_start_at.isoformat()
|
|
57
|
+
if hasattr(session_start_at, "isoformat")
|
|
58
|
+
else str(session_start_at)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# capture the final values from existing metadata if we didn't overwrite
|
|
62
|
+
if final_reset_at is None and "reset_at" in data:
|
|
63
|
+
from .cooldown import parse_iso_datetime
|
|
64
|
+
try:
|
|
65
|
+
final_reset_at = parse_iso_datetime(data["reset_at"])
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
if final_session_start_at is None and "session_start_at" in data:
|
|
69
|
+
from .cooldown import parse_iso_datetime
|
|
70
|
+
try:
|
|
71
|
+
final_session_start_at = parse_iso_datetime(data["session_start_at"])
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
if quota_text is not None:
|
|
76
|
+
data["quota_text"] = quota_text
|
|
77
|
+
if quota_percent_left is not None:
|
|
78
|
+
data["quota_percent_left"] = quota_percent_left
|
|
79
|
+
data["is_expired"] = is_expired
|
|
80
|
+
data["updated_at"] = datetime.now().astimezone().isoformat()
|
|
81
|
+
metadata_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
82
|
+
console.print(
|
|
83
|
+
f"Updated local metadata for [cyan]{email}[/]: [dim]{metadata_path.name}[/]"
|
|
84
|
+
)
|
|
85
|
+
except Exception as exc:
|
|
86
|
+
console.print(f"[yellow]Warning:[/] Failed to patch local metadata: {exc}")
|
|
87
|
+
else:
|
|
88
|
+
now = datetime.now().astimezone()
|
|
89
|
+
final_reset_at = reset_at or now
|
|
90
|
+
final_session_start_at = session_start_at or (now - timedelta(days=7))
|
|
91
|
+
archive_name = build_archive_name(final_reset_at, email)
|
|
92
|
+
metadata_path = backup_dir / archive_name.replace(".tar.gz", ".metadata.json")
|
|
93
|
+
data = {
|
|
94
|
+
"product": "codex",
|
|
95
|
+
"email": email,
|
|
96
|
+
"session_start_at": (
|
|
97
|
+
final_session_start_at.isoformat()
|
|
98
|
+
if hasattr(final_session_start_at, "isoformat")
|
|
99
|
+
else str(final_session_start_at)
|
|
100
|
+
),
|
|
101
|
+
"next_available_at": (
|
|
102
|
+
final_reset_at.isoformat() if hasattr(final_reset_at, "isoformat") else str(final_reset_at)
|
|
103
|
+
),
|
|
104
|
+
"reset_at": (
|
|
105
|
+
final_reset_at.isoformat() if hasattr(final_reset_at, "isoformat") else str(final_reset_at)
|
|
106
|
+
),
|
|
107
|
+
"quota_text": quota_text or "unknown",
|
|
108
|
+
"quota_percent_left": quota_percent_left,
|
|
109
|
+
"is_expired": is_expired,
|
|
110
|
+
"archive_name": archive_name,
|
|
111
|
+
"created_at": now.isoformat(),
|
|
112
|
+
"status_source": "pre_switch_sync",
|
|
113
|
+
"metadata_only": True,
|
|
114
|
+
}
|
|
115
|
+
try:
|
|
116
|
+
metadata_path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
117
|
+
console.print(
|
|
118
|
+
f"Created cooldown-only metadata for [cyan]{email}[/]: [dim]{metadata_path.name}[/]"
|
|
119
|
+
)
|
|
120
|
+
except Exception as exc:
|
|
121
|
+
console.print(f"[yellow]Warning:[/] Failed to create local metadata: {exc}")
|
|
122
|
+
|
|
123
|
+
update_registry_entry(
|
|
124
|
+
email=email,
|
|
125
|
+
reset_at=final_reset_at,
|
|
126
|
+
is_expired=is_expired,
|
|
127
|
+
quota_text=quota_text,
|
|
128
|
+
quota_percent_left=quota_percent_left,
|
|
129
|
+
session_start_at=final_session_start_at,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if args and getattr(args, "cloud", False):
|
|
133
|
+
cp = get_cloud_provider(args)
|
|
134
|
+
if cp:
|
|
135
|
+
sync_registry_with_cloud(cp)
|
|
136
|
+
entries = list_cloud_backups(cp, email=email, latest_per_email=True)
|
|
137
|
+
if entries:
|
|
138
|
+
selected = entries[0]
|
|
139
|
+
archive_name = selected.archive_path.name
|
|
140
|
+
metadata_name = archive_name.replace(".tar.gz", ".metadata.json")
|
|
141
|
+
|
|
142
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
143
|
+
local_metadata = Path(tmp) / metadata_name
|
|
144
|
+
try:
|
|
145
|
+
cp.download_file(metadata_name, local_metadata)
|
|
146
|
+
data = json.loads(local_metadata.read_text(encoding="utf-8"))
|
|
147
|
+
data["reset_at"] = (
|
|
148
|
+
reset_at.isoformat() if hasattr(reset_at, "isoformat") else str(reset_at)
|
|
149
|
+
)
|
|
150
|
+
data["next_available_at"] = data["reset_at"]
|
|
151
|
+
if session_start_at:
|
|
152
|
+
data["session_start_at"] = (
|
|
153
|
+
session_start_at.isoformat()
|
|
154
|
+
if hasattr(session_start_at, "isoformat")
|
|
155
|
+
else str(session_start_at)
|
|
156
|
+
)
|
|
157
|
+
data["quota_text"] = quota_text
|
|
158
|
+
if quota_percent_left is not None:
|
|
159
|
+
data["quota_percent_left"] = quota_percent_left
|
|
160
|
+
data["is_expired"] = is_expired
|
|
161
|
+
data["updated_at"] = datetime.now().astimezone().isoformat()
|
|
162
|
+
local_metadata.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
163
|
+
|
|
164
|
+
console.print(
|
|
165
|
+
f"Uploading updated metadata to Cloud: [dim]{metadata_name}[/] ..."
|
|
166
|
+
)
|
|
167
|
+
cp.upload_file(local_metadata, metadata_name)
|
|
168
|
+
console.print("Cloud metadata update complete.")
|
|
169
|
+
except Exception as exc:
|
|
170
|
+
console.print(f"[yellow]Warning:[/] Failed to patch cloud metadata: {exc}")
|
|
171
|
+
else:
|
|
172
|
+
console.print("[yellow]Warning:[/] Cloud update requested but credentials not resolved.")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def sync_current_account_status(args: Any) -> None:
|
|
176
|
+
codex_home = Path(
|
|
177
|
+
getattr(
|
|
178
|
+
args,
|
|
179
|
+
"dest_dir",
|
|
180
|
+
args.source_dir if hasattr(args, "source_dir") else "~/.codex",
|
|
181
|
+
)
|
|
182
|
+
).expanduser()
|
|
183
|
+
auth_path = codex_home / "auth.json"
|
|
184
|
+
|
|
185
|
+
current_email = None
|
|
186
|
+
if auth_path.exists():
|
|
187
|
+
try:
|
|
188
|
+
auth_data = json.loads(auth_path.read_text(encoding="utf-8"))
|
|
189
|
+
current_email = auth_data.get("email")
|
|
190
|
+
except Exception:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
if not current_email:
|
|
194
|
+
console.print(
|
|
195
|
+
"[yellow]Warning:[/] Could not identify current account from auth.json. "
|
|
196
|
+
"Skipping pre-switch status sync."
|
|
197
|
+
)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if getattr(args, "without_status_check", False):
|
|
201
|
+
now = datetime.now().astimezone()
|
|
202
|
+
session_start_at = now
|
|
203
|
+
reset_at = now + timedelta(days=7)
|
|
204
|
+
|
|
205
|
+
console.print(f"[yellow]Note:[/] Bypassing status check for [cyan]{current_email}[/].")
|
|
206
|
+
console.print(
|
|
207
|
+
"[yellow]Assuming exhaustion:[/] Next reset estimated for "
|
|
208
|
+
f"[bright_magenta]{reset_at.strftime('%Y-%m-%d %H:%M:%S')}[/]"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
patch_metadata(
|
|
212
|
+
email=current_email,
|
|
213
|
+
reset_at=reset_at,
|
|
214
|
+
quota_text="Status capture bypassed via --without-status-check. Estimated +7 days cooldown.",
|
|
215
|
+
quota_percent_left=None,
|
|
216
|
+
args=args,
|
|
217
|
+
session_start_at=session_start_at,
|
|
218
|
+
)
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
console.print(f"Syncing status for current account: [cyan]{current_email}[/]")
|
|
222
|
+
|
|
223
|
+
text = None
|
|
224
|
+
from .status import TokenExpiredError
|
|
225
|
+
for attempt in range(1, 3):
|
|
226
|
+
try:
|
|
227
|
+
text = read_status_text_from_args(args)
|
|
228
|
+
if text:
|
|
229
|
+
break
|
|
230
|
+
except TokenExpiredError as exc:
|
|
231
|
+
console.print(f"[bold red]Error:[/] {exc}")
|
|
232
|
+
# Try to at least get the email from the error output
|
|
233
|
+
try:
|
|
234
|
+
from .status import parse_live_status_text
|
|
235
|
+
status = parse_live_status_text(exc.output)
|
|
236
|
+
patch_metadata(
|
|
237
|
+
email=status.email,
|
|
238
|
+
reset_at=None,
|
|
239
|
+
quota_text="TOKEN EXPIRED: Re-login required.",
|
|
240
|
+
quota_percent_left=None,
|
|
241
|
+
args=args,
|
|
242
|
+
session_start_at=None,
|
|
243
|
+
is_expired=True,
|
|
244
|
+
)
|
|
245
|
+
except Exception:
|
|
246
|
+
# If we can't even get the email, we use the one from auth.json
|
|
247
|
+
patch_metadata(
|
|
248
|
+
email=current_email,
|
|
249
|
+
reset_at=None,
|
|
250
|
+
quota_text="TOKEN EXPIRED: Re-login required.",
|
|
251
|
+
quota_percent_left=None,
|
|
252
|
+
args=args,
|
|
253
|
+
is_expired=True,
|
|
254
|
+
)
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
if attempt == 1:
|
|
258
|
+
console.print(f"[yellow]Status capture failed (attempt 1): {exc}. Try one more time...[/]")
|
|
259
|
+
else:
|
|
260
|
+
console.print(
|
|
261
|
+
f"[bold red]Error:[/] Status capture failed twice for [cyan]{current_email}[/]: {exc}"
|
|
262
|
+
)
|
|
263
|
+
console.print("\n[bold yellow]Next-Gen Safety Protocol:[/]")
|
|
264
|
+
console.print("If Codex has changed its layout or status is unavailable, you MUST use:")
|
|
265
|
+
console.print(f" [bright_cyan]cm {args.command} --without-status-check ...[/]")
|
|
266
|
+
console.print("[dim]This will safely assume a 7-day cooldown for the current account.[/]")
|
|
267
|
+
sys.exit(1)
|
|
268
|
+
|
|
269
|
+
if text:
|
|
270
|
+
try:
|
|
271
|
+
status = parse_live_status_text(
|
|
272
|
+
text,
|
|
273
|
+
reference_year=getattr(args, "reference_year", None),
|
|
274
|
+
)
|
|
275
|
+
patch_metadata(
|
|
276
|
+
email=status.email,
|
|
277
|
+
reset_at=status.reset_at,
|
|
278
|
+
quota_text=status.quota_text,
|
|
279
|
+
quota_percent_left=status.quota_percent_left,
|
|
280
|
+
args=args,
|
|
281
|
+
session_start_at=status.session_start_at,
|
|
282
|
+
is_expired=status.is_expired,
|
|
283
|
+
)
|
|
284
|
+
except Exception as exc:
|
|
285
|
+
console.print(
|
|
286
|
+
f"[bold red]Error:[/] Failed to parse status for [cyan]{current_email}[/]: {exc}"
|
|
287
|
+
)
|
|
288
|
+
console.print("[dim]Use --without-status-check if Codex layout has changed.[/]")
|
|
289
|
+
sys.exit(1)
|