cdx-manager 0.18.0 → 0.18.2

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CDX Manager
2
2
 
3
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.18.0-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
3
+ [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.18.2-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Stop guessing which AI account still has quota.** `cdx` tracks the rate-limit window of every Codex and Claude account you own, tells you which one is usable right now, and launches it with isolated auth — in one command.
6
6
 
@@ -560,6 +560,7 @@ They are deliberately hard to become noise. One hint per invocation at most, not
560
560
 
561
561
  The companion is a native binary distributed as a release asset, not part of the pip package. Its trust model is worth stating plainly rather than leaving you to assume the usual one.
562
562
 
563
+ - **Where the checksum comes from.** Releases older than the one you are running are vouched for by `checksums/release-archives.json`, committed to the repository before those assets existed. Your own release is a special case: its tray assets are built after the tag, so no package built at tag time can carry their checksums. For that one version CDX reads the ledger the release publishes beside the assets — the publisher's own assertion over the same channel, which catches a truncated download or a wrong-architecture asset but does not defend against someone who can rewrite the release itself.
563
564
  - **The checksum is the vouching, not Apple or Microsoft.** The macOS bundle is signed with a *self-signed* certificate. That signature exists because Apple Silicon refuses to execute unsigned arm64 code at all — it is not a claim that Apple reviewed anything. What actually vouches for the asset is the SHA-256 published in `checksums/release-archives.json`, verified before a single byte is unpacked. An asset with no published checksum is refused rather than installed hopefully.
564
565
  - **Install through `cdx tray install`, and no download link is offered.** This is a security property, not a convenience. A file fetched by CDX carries no macOS quarantine attribute and no Windows Mark-of-the-Web, so nothing puts Gatekeeper or SmartScreen between you and a companion you asked for. The same file fetched through a browser would carry both. That is why no direct download URL is published: it would be a worse path wearing the same clothes.
565
566
  - **Nothing is silently replaced.** An update downloads, verifies, and *proves the replacement starts* before the working companion is touched. A corrupt asset or one built for the wrong architecture costs an error message, not a tray you can no longer launch. An interrupted update is a named, recoverable state that `cdx tray doctor` reports.
@@ -0,0 +1,50 @@
1
+ # CDX Manager 0.18.1
2
+
3
+ ## Fixes
4
+
5
+ ### `cdx tray install` works against a published release
6
+
7
+ 0.18.0 shipped a tray installer that refused every asset the release published.
8
+ Three separate causes, each of which only appeared when installing from a real
9
+ release rather than from a locally built archive.
10
+
11
+ **The archive shape was rejected.** Release assets are written with
12
+ `tar -C dir .`, so they carry a member for the directory itself. The guard that
13
+ refuses an archive escaping its install directory treated that member as an
14
+ escape, when it resolves to the destination and nothing else. The destination is
15
+ now accepted; an archive that genuinely writes outside its directory is still
16
+ refused, and a link member still is.
17
+
18
+ **Windows asked for an asset nobody builds.** CDX looked for
19
+ `x86_64-pc-windows-gnu` while releases are built with the MSVC toolchain, so no
20
+ published checksum ever matched the name it asked for. Every Windows install
21
+ refused for want of a checksum that was published under a different target.
22
+
23
+ **ARM Linux was offered and never built.** `aarch64-unknown-linux-musl` was a
24
+ target CDX would install and the release did not produce. It is now built and
25
+ published alongside the others.
26
+
27
+ A test now asserts that every target CDX offers is one the release actually
28
+ builds, so this class of mismatch fails in CI rather than on a user's machine.
29
+
30
+ ### Windows toasts carry the CDX icon
31
+
32
+ The Start Menu shortcut CDX installs now points at an icon shipped with the
33
+ companion, so a notification shows the CDX logo rather than a generic one.
34
+ Uninstall removes the shortcut along with the companion.
35
+
36
+ ## Release safety
37
+
38
+ Publishing to npm and PyPI now waits for the tray companion assets to build
39
+ successfully. All four workflows fire on the same release event, so previously
40
+ they ran beside each other: a failed asset build would have left both registries
41
+ carrying a version whose release had no companion to install.
42
+
43
+ ## Validation
44
+
45
+ - `npm run lint`
46
+ - `npm test` — 771 Python tests
47
+ - `cargo test` — 45 Rust tests; `cargo clippy` clean with zero warnings on three
48
+ targets
49
+ - `cdx tray install`, `doctor`, `launch` and `uninstall` exercised against the
50
+ published v0.18.0 assets on macOS arm64, Windows, and Linux under WSL
@@ -0,0 +1,43 @@
1
+ # CDX Manager 0.18.2
2
+
3
+ ## Fixes
4
+
5
+ ### `cdx tray install` can install the companion for the release you are running
6
+
7
+ No release could ever install its own tray companion, and the cause was
8
+ structural rather than a bug. Tray assets are built after the tag exists, so
9
+ their checksums are recorded afterwards — which means a package built at tag
10
+ time ships a checksum ledger that stops at the previous release. Installing
11
+ refused with "No published checksum for … of CDX <this version>".
12
+
13
+ The release already publishes `release-archives.json` beside the assets. A
14
+ missing local entry now falls through to it.
15
+
16
+ What that is worth, stated plainly: it is the publisher's own assertion fetched
17
+ over the same HTTPS channel as the asset, so it does not defend against someone
18
+ able to rewrite the release. It does defend against a truncated download and an
19
+ asset built for another architecture, which are the failures that happen. A
20
+ checksum committed to the repository before the asset existed is the stronger
21
+ claim, so a local entry always wins and fetches nothing.
22
+
23
+ ### The PyPI package can start
24
+
25
+ `pyproject.toml` declared only the root package, so `cdx_manager.commands` was
26
+ absent from every wheel since it was extracted and `pip install cdx-manager`
27
+ produced a CLI that failed at import. setuptools does not warn about a
28
+ subpackage it was never told about: the wheel builds cleanly and breaks on the
29
+ user's machine.
30
+
31
+ The npm package and the standalone installer were unaffected — both ship the
32
+ source tree wholesale.
33
+
34
+ A test now asserts every package under `src/` is declared for the wheel.
35
+
36
+ ## Validation
37
+
38
+ - `npm run lint`
39
+ - `npm test` — 775 Python tests
40
+ - A wheel built from this tree installs into a clean virtualenv and runs
41
+ `cdx --version` and `cdx tray doctor`
42
+ - `cdx tray install` exercised against a checksum ledger with no entry for the
43
+ version, which is the shape every shipped package has
@@ -192,6 +192,27 @@
192
192
  "v0.17.1": {
193
193
  "github_tarball_sha256": "f1cc36d55f17d8e5c8ad0b0ebc419e812b8788742c3e45ce1137303924114dd7",
194
194
  "github_zip_sha256": "29acb306ad76535d0db701504a53b9887b8a6b0b95d968634139f85952a34ec3"
195
+ },
196
+ "v0.18.0": {
197
+ "github_tarball_sha256": "ae666d8ec38a74859e1edc5023101474a2fb647ecb3ba285fd746c7b84699854",
198
+ "github_zip_sha256": "72ecdd02b449c2256a74f77afec7eda8cd9b53b52bb62858e45d3d6000a1bd24",
199
+ "tray_assets": {
200
+ "aarch64-apple-darwin": "45aa98624bda29dea26637aeb22a86b22fe018a95fb176bfffd3504c772c9988",
201
+ "x86_64-apple-darwin": "d310852919cd4e2ddce2108044d0039e088b5bdc3dfa029be4d81ee0df1c420b",
202
+ "x86_64-pc-windows-msvc": "86e29cbdbcf070aec7fd34e5596343a43f2b5092e634ca094aba96abf1d49884",
203
+ "x86_64-unknown-linux-musl": "36ef879c0e75f3c367ed48f1f5a67e25698ec5a04c70958b48ae0885f87487d8"
204
+ }
205
+ },
206
+ "v0.18.1": {
207
+ "github_tarball_sha256": "260529247233abca840ecba03c2948234425b4fe0d27f2e5c008bdeb89fb0bd5",
208
+ "github_zip_sha256": "bc14fc2fd27641a56089a023b51427abac854eaf527a17d9bc0571698fb19b9f",
209
+ "tray_assets": {
210
+ "aarch64-apple-darwin": "8a1cd7f819d21097d63f8c452479bae7bfbfe3468473c7e8f557043c7b7ab47c",
211
+ "aarch64-unknown-linux-musl": "a4a757bfc0f048aa24439b675cb9f269d70328a98be88efbe5cc3fea96cbd78d",
212
+ "x86_64-apple-darwin": "1003b7051d6dfbf1a19c1e82eca1c648d11363171a77ad0cd85822a167921573",
213
+ "x86_64-pc-windows-msvc": "738eeb165c1c2fde58bd952cdc0a59561ca96856a3fb8d0cffc024bfb78b9ca6",
214
+ "x86_64-unknown-linux-musl": "0b3c02b007aa0655a5b95c7794eba49a37bdb9868c447bb0cfbdfbd9d371d094"
215
+ }
195
216
  }
196
217
  }
197
218
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cdx-manager"
7
- version = "0.18.0"
7
+ version = "0.18.2"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -45,10 +45,17 @@ Changelog = "https://github.com/AlexAgo83/cdx-manager/tree/main/changelogs"
45
45
  cdx = "cdx_manager.cli:cli_entry"
46
46
 
47
47
  [tool.setuptools]
48
- packages = ["cdx_manager"]
48
+ # Every package under src/, not just the root. Subpackages have to be named
49
+ # explicitly here, and one that is missing is not a build error: the wheel is
50
+ # produced happily and fails at import on the user's machine. src/commands has
51
+ # been absent from every wheel since it was extracted, so `pip install
52
+ # cdx-manager` gave an unusable CLI while the npm and standalone installs — both
53
+ # of which ship the source tree wholesale — worked fine.
54
+ packages = ["cdx_manager", "cdx_manager.commands"]
49
55
 
50
56
  [tool.setuptools.package-dir]
51
57
  cdx_manager = "src"
58
+ "cdx_manager.commands" = "src/commands"
52
59
 
53
60
  [tool.pytest.ini_options]
54
61
  testpaths = ["test"]
@@ -32,7 +32,11 @@ CHECKSUM_LEDGER = os.path.join(
32
32
  TARGETS = {
33
33
  ("Darwin", "arm64"): "aarch64-apple-darwin",
34
34
  ("Darwin", "x86_64"): "x86_64-apple-darwin",
35
- ("Windows", "AMD64"): "x86_64-pc-windows-gnu",
35
+ # msvc, not gnu: this has to name the asset the release actually publishes,
36
+ # and the release workflow builds on windows-latest with the default MSVC
37
+ # toolchain. A mismatch here is not a degraded install, it is a refusal —
38
+ # there is no checksum published under a target nobody built.
39
+ ("Windows", "AMD64"): "x86_64-pc-windows-msvc",
36
40
  ("Linux", "x86_64"): "x86_64-unknown-linux-musl",
37
41
  ("Linux", "aarch64"): "aarch64-unknown-linux-musl",
38
42
  }
@@ -56,19 +60,61 @@ def asset_name(version, target):
56
60
  return f"cdx-tray-{version}-{target}.tar.gz"
57
61
 
58
62
 
59
- def expected_checksum(version, target, ledger_path=CHECKSUM_LEDGER):
63
+ RELEASE_LEDGER_URL = (
64
+ "https://github.com/AlexAgo83/cdx-manager/releases/download/v{version}/release-archives.json"
65
+ )
66
+
67
+
68
+ def _ledger_entry(ledger, version, target):
69
+ release = (ledger.get("releases") or {}).get(f"v{version}") or {}
70
+ return (release.get("tray_assets") or {}).get(target)
71
+
72
+
73
+ def expected_checksum(version, target, ledger_path=CHECKSUM_LEDGER, download=None):
60
74
  """The published sha256 for this asset, or None when none is recorded.
61
75
 
62
76
  None is a refusal, not a fallback. An asset with no published checksum is
63
77
  exactly the case this gate exists for.
78
+
79
+ The shipped ledger cannot contain the entry for its own release, and that is
80
+ structural rather than an oversight: the tray assets are built after the tag
81
+ exists, so their checksums are recorded afterwards. A package built at tag
82
+ time therefore ships a ledger that stops at the previous release, and
83
+ `cdx tray install` would refuse the companion for the very version the user
84
+ is running.
85
+
86
+ So a missing local entry falls through to the ledger the release publishes
87
+ alongside the assets. Be clear about what that is worth: it is the
88
+ publisher's own assertion, fetched over the same HTTPS channel as the asset,
89
+ so it does not defend against someone who can rewrite the release. It does
90
+ defend against a truncated or corrupted download and against an asset built
91
+ for another architecture, which are the failures that actually happen. The
92
+ strong guarantee — a checksum committed before the asset existed — is what
93
+ the local ledger gives for every release older than this one.
64
94
  """
65
95
  try:
66
96
  with open(ledger_path, encoding="utf-8") as handle:
67
97
  ledger = json.load(handle)
68
98
  except (OSError, ValueError):
69
- return None
70
- release = (ledger.get("releases") or {}).get(f"v{version}") or {}
71
- return (release.get("tray_assets") or {}).get(target)
99
+ ledger = {}
100
+ local = _ledger_entry(ledger, version, target)
101
+ if local:
102
+ return local
103
+ return _published_checksum(version, target, download=download)
104
+
105
+
106
+ def _published_checksum(version, target, download=None):
107
+ """The entry from the ledger attached to the release, or None."""
108
+ fetch = download or _download
109
+ with tempfile.TemporaryDirectory(prefix="cdx-tray-ledger-") as scratch:
110
+ destination = os.path.join(scratch, "release-archives.json")
111
+ try:
112
+ fetch(RELEASE_LEDGER_URL.format(version=version), destination)
113
+ with open(destination, encoding="utf-8") as handle:
114
+ published = json.load(handle)
115
+ except (OSError, ValueError):
116
+ return None
117
+ return _ledger_entry(published, version, target)
72
118
 
73
119
 
74
120
  def _sha256(path):
@@ -143,7 +189,7 @@ def install(base_dir, version, download=None, ledger_path=CHECKSUM_LEDGER, targe
143
189
  raise TrayInstallError(
144
190
  f"No tray companion is published for {platform.system()} {platform.machine()}."
145
191
  )
146
- expected = expected_checksum(version, target, ledger_path=ledger_path)
192
+ expected = expected_checksum(version, target, ledger_path=ledger_path, download=download)
147
193
  if not expected:
148
194
  raise TrayInstallError(
149
195
  f"No published checksum for the {target} tray companion of CDX {version}. "
@@ -214,9 +260,14 @@ def _extract(archive, destination):
214
260
  """
215
261
  names = []
216
262
  with tarfile.open(archive, "r:gz") as tar:
263
+ root = os.path.realpath(destination)
217
264
  for member in tar.getmembers():
218
265
  resolved = os.path.realpath(os.path.join(destination, member.name))
219
- if not resolved.startswith(os.path.realpath(destination) + os.sep):
266
+ # The destination itself is not an escape. An archive written with
267
+ # `tar -C dir .` carries a `.` member for the directory, which is
268
+ # what the release workflow produces — and rejecting it refused
269
+ # every published asset while the archive was perfectly safe.
270
+ if resolved != root and not resolved.startswith(root + os.sep):
220
271
  raise TrayInstallError(
221
272
  f"{os.path.basename(archive)} tries to write outside its install directory "
222
273
  f"({member.name}). Nothing was installed."
@@ -254,7 +305,11 @@ def _executable_in(destination, names):
254
305
  bundles = [n for n in names if candidate(n).endswith(".app")]
255
306
  binaries = [n for n in names if candidate(n) in ("cdx-tray", "cdx-tray.exe")]
256
307
  for name in bundles + binaries:
257
- path = os.path.join(destination, name.rstrip("/"))
308
+ # Normalised because an archive written with `tar -C dir .` names its
309
+ # members `./CDX.app`, and this path is recorded, launched, and removed
310
+ # later — a `/./` in the middle works but reads like a defect to anyone
311
+ # looking at the install record or a doctor report.
312
+ path = os.path.normpath(os.path.join(destination, name.rstrip("/")))
258
313
  if os.path.isdir(path):
259
314
  return path
260
315
  if os.path.isfile(path):