cdx-manager 0.3.4 → 0.4.1

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,12 +1,15 @@
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.4.1-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
+
3
5
  **Run multiple Codex and Claude sessions from one terminal. Switch between accounts instantly.**
4
6
 
5
7
  If you use AI coding tools at scale ; multiple accounts, multiple providers : you know the friction: re-authenticating, losing context, juggling environment variables. `cdx` removes all of that.
6
8
 
7
9
  One command to launch any session. Zero auth juggling.
8
10
 
9
- [![License](https://img.shields.io/badge/license-MIT-4C8BF5)](LICENSE) ![Version](https://img.shields.io/badge/version-v0.3.4-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
11
+ <img width="213" height="227" alt="image" src="https://github.com/user-attachments/assets/f15f449c-d23e-47fe-a455-17c7386f9be2" />
12
+ <img width="645" height="129" alt="image" src="https://github.com/user-attachments/assets/34bcb395-f832-4da6-9247-3e5022e75e56" />
10
13
 
11
14
  ---
12
15
 
@@ -66,10 +69,12 @@ One command to launch any session. Zero auth juggling.
66
69
 
67
70
  ### Prerequisites
68
71
 
72
+ - Node.js 18+ with npm
69
73
  - Python 3.9+
70
- - npm
71
74
  - `codex` and/or `claude` CLI installed and available in your PATH
72
75
 
76
+ On Windows, the npm launcher looks for Python in this order: `py -3`, `python`, then `python3`. Make sure at least one of those commands resolves to Python 3.
77
+
73
78
  ### Install
74
79
 
75
80
  From npm:
@@ -99,19 +104,24 @@ npm install -g cdx-manager
99
104
  With the standalone PowerShell installer:
100
105
 
101
106
  ```powershell
102
- irm https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.ps1 | iex
107
+ Invoke-WebRequest https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.ps1 -OutFile install.ps1
108
+ # Optional: set CDX_SHA256 before running if you have a trusted checksum
109
+ powershell -ExecutionPolicy Bypass -File .\install.ps1
103
110
  ```
104
111
 
105
112
  With the standalone GitHub installer:
106
113
 
107
114
  ```bash
108
- curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh | sh
115
+ curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
116
+ # Optional: set CDX_SHA256 before running if you have a trusted checksum
117
+ sh install.sh
109
118
  ```
110
119
 
111
120
  For a specific version:
112
121
 
113
122
  ```bash
114
- curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh | CDX_VERSION=v0.3.4 sh
123
+ curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
124
+ CDX_VERSION=v0.4.1 sh install.sh
115
125
  ```
116
126
 
117
127
  From source:
@@ -150,6 +160,13 @@ Alternatively, for a non-symlinked global source install:
150
160
  npm install -g .
151
161
  ```
152
162
 
163
+ Security note:
164
+
165
+ - The standalone installers try to resolve official release checksums from `checksums/release-archives.json`.
166
+ - You can still override verification explicitly through `CDX_SHA256`.
167
+ - Prefer `npm`, `pipx`, or `uv` when you want registry-backed install flows.
168
+ - If you use the standalone script, download it first, inspect it, and prefer a release with an official checksum entry.
169
+
153
170
  ### Environment
154
171
 
155
172
  By default, `cdx` stores all data under `~/.cdx/`. Override with:
@@ -220,11 +237,13 @@ cdx status
220
237
  | `cdx logout <name> [--json]` | Log out of a session |
221
238
  | `cdx rmv <name> [--force] [--json]` | Remove a session and its auth data (prompts for confirmation unless `--force`) |
222
239
  | `cdx clean [name] [--json]` | Clear launch transcript logs for one session or all sessions |
240
+ | `cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Export sessions to a portable bundle; `--include-auth` encrypts auth data with a passphrase |
241
+ | `cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Import sessions from a bundle into the current `CDX_HOME` |
223
242
  | `cdx doctor [--json]` | Inspect CLI dependencies, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines |
224
243
  | `cdx repair [--dry-run] [--force] [--json]` | Plan or apply safe repairs for missing state files, quarantines, and orphan profiles |
225
- | `cdx notify <name> --at-reset [--poll seconds] [--once]` | Wait for a session reset time and send a desktop notification when due |
226
- | `cdx notify --next-ready [--poll seconds] [--once]` | Wait until the recommended session is usable or needs a refresh after reset |
227
- | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON keeps the same row-array shape and writes live Claude refresh warnings to stderr |
244
+ | `cdx notify <name> --at-reset [--poll seconds] [--once] [--json]` | Wait for a session reset time and send a desktop notification when due |
245
+ | `cdx notify --next-ready [--poll seconds] [--once] [--json]` | Wait until the recommended session is usable or needs a refresh after reset |
246
+ | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
228
247
  | `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
229
248
  | `cdx status <name> [--json] [--refresh]` | Show detailed usage breakdown for one session |
230
249
  | `cdx --help` | Show usage |
@@ -246,6 +265,8 @@ Commands with machine-readable output:
246
265
  - `cdx ren ... --json`
247
266
  - `cdx rmv ... --json`
248
267
  - `cdx clean ... --json`
268
+ - `cdx export ... --json`
269
+ - `cdx import ... --json`
249
270
  - `cdx login ... --json`
250
271
  - `cdx logout ... --json`
251
272
  - `cdx doctor --json`
@@ -256,6 +277,7 @@ Success payloads follow a shared envelope:
256
277
 
257
278
  ```json
258
279
  {
280
+ "schema_version": 1,
259
281
  "ok": true,
260
282
  "action": "add",
261
283
  "message": "Created session work (codex)",
@@ -270,6 +292,7 @@ Errors use a shared stderr JSON envelope whenever `--json` is present:
270
292
 
271
293
  ```json
272
294
  {
295
+ "schema_version": 1,
273
296
  "ok": false,
274
297
  "error": {
275
298
  "code": "invalid_usage",
@@ -279,15 +302,44 @@ Errors use a shared stderr JSON envelope whenever `--json` is present:
279
302
  }
280
303
  ```
281
304
 
305
+ `status --json` and similar commands also use the same envelope and place non-fatal issues in `warnings` instead of mixing plain-text diagnostics into `stderr`.
306
+
282
307
  This makes `cdx-manager` usable from editor plugins, scripts, and desktop apps without scraping human-readable terminal output.
283
308
 
284
309
  ---
285
310
 
311
+ ## Backup And Restore
312
+
313
+ You can move sessions between machines with portable bundles:
314
+
315
+ ```bash
316
+ cdx export backup.cdx
317
+ cdx import backup.cdx
318
+ ```
319
+
320
+ To migrate auth and avoid logging in again, include auth data in an encrypted bundle:
321
+
322
+ ```bash
323
+ export CDX_BUNDLE_PASSPHRASE='choose-a-strong-passphrase'
324
+ cdx export backup-auth.cdx --include-auth --passphrase-env CDX_BUNDLE_PASSPHRASE
325
+ cdx import backup-auth.cdx --passphrase-env CDX_BUNDLE_PASSPHRASE
326
+ ```
327
+
328
+ Notes:
329
+
330
+ - `--include-auth` is encrypted and requires a passphrase.
331
+ - Without `--passphrase-env`, `cdx` prompts in an interactive terminal.
332
+ - `--sessions work,perso` exports or imports only a subset.
333
+ - `--force` allows overwriting existing destination sessions during import or replacing an existing bundle file during export.
334
+ - Auth bundles contain credentials. Treat them like secrets and delete them after transfer.
335
+
336
+ ---
337
+
286
338
  ## Available Scripts
287
339
 
288
340
  - `npm test`: run the Python test suite
289
- - `npm run test:py`: run the Python unit tests directly
290
- - `npm run lint`: byte-compile the Python sources and tests
341
+ - `npm run test:py`: run the Python unit tests through the portable launcher
342
+ - `npm run lint`: check the Node launcher and byte-compile the Python sources and tests
291
343
  - `npm run link`: link `cdx` globally for local development (`npm link`)
292
344
  - `npm run unlink`: remove the global link
293
345
 
@@ -300,6 +352,7 @@ This makes `cdx-manager` usable from editor plugins, scripts, and desktop apps w
300
352
  - `pipx install cdx-manager`
301
353
  - `uv tool install cdx-manager`
302
354
  - `install.ps1`
355
+ - The npm launcher resolves Python via `py -3`, `python`, then `python3`, so a global npm install works even when `python3.exe` is missing.
303
356
  - `install.sh` is Unix-only.
304
357
  - `make install` and `make uninstall` are Unix-oriented convenience commands, not the default Windows path.
305
358
  - `cdx` isolates Claude sessions on Windows by setting `HOME`, `USERPROFILE`, `HOMEDRIVE`, and `HOMEPATH`.
@@ -315,12 +368,15 @@ This makes `cdx-manager` usable from editor plugins, scripts, and desktop apps w
315
368
 
316
369
  ```text
317
370
  bin/
318
- cdx # Entry point shebang + main() call
371
+ cdx.js # Node launcher used by npm
372
+ python-runner.js # Shared Python resolver and process wrapper
373
+ cdx # Python entrypoint invoked by the launcher
319
374
 
320
375
  src/
321
376
  cli.py # Top-level command router
322
377
  cli_commands.py # Command handlers and argument handling
323
378
  cli_render.py # Terminal formatting, tables, colors, and errors
379
+ backup_bundle.py # Portable session bundle encoding/decoding + auth encryption
324
380
  status_view.py # Status table/detail rendering and priority ranking
325
381
  provider_runtime.py # Provider launch/auth commands, transcripts, signals
326
382
  claude_refresh.py # Claude usage refresh orchestration
@@ -368,6 +424,7 @@ Session names are URL-encoded when used as directory or file names. CLI command
368
424
  ## Troubleshooting
369
425
 
370
426
  - **`cdx <name>` fails with "not authenticated"** — run `cdx login <name>` first.
427
+ - **`cdx` says no compatible Python 3 interpreter was found** — install Python 3 and make `py -3`, `python`, or `python3` available on PATH.
371
428
  - **`cdx add` succeeds but the session does not appear** — check that `CDX_HOME` is consistent between calls; a mismatch creates two separate registries.
372
429
  - **Status shows `n/a` for all fields** — the session has not been launched yet, or the provider has not written any status output to its history files. Launch the session and run `/status` inside it at least once.
373
430
  - **`cdx rmv` says "Removal requires confirmation in an interactive terminal"** — pass `--force` to bypass the prompt in non-interactive environments (scripts, CI).
package/bin/cdx.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require("node:path");
4
+ const { runPython } = require("./python-runner");
5
+
6
+ const scriptPath = path.join(__dirname, "cdx");
7
+ const exitCode = runPython([scriptPath, ...process.argv.slice(2)], { expandGlobs: false });
8
+
9
+ process.exit(exitCode);
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const os = require("node:os");
5
+ const path = require("node:path");
6
+ const { spawnSync } = require("node:child_process");
7
+
8
+ const PYTHON_VERSION_CHECK = "import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)";
9
+ const PYTHON_CACHE_PREFIX = path.join(os.tmpdir(), "cdx-manager-pycache");
10
+ const SIGNAL_EXIT_CODES = {
11
+ SIGHUP: 129,
12
+ SIGINT: 130,
13
+ SIGTERM: 143,
14
+ };
15
+
16
+ const WINDOWS_CANDIDATES = [
17
+ { command: "py", args: ["-3"], label: "py -3" },
18
+ { command: "python", args: [], label: "python" },
19
+ { command: "python3", args: [], label: "python3" },
20
+ ];
21
+
22
+ const UNIX_CANDIDATES = [
23
+ { command: "python3", args: [], label: "python3" },
24
+ { command: "python", args: [], label: "python" },
25
+ ];
26
+
27
+ function getCandidates(platform = process.platform) {
28
+ return platform === "win32" ? WINDOWS_CANDIDATES : UNIX_CANDIDATES;
29
+ }
30
+
31
+ function probeCandidate(candidate) {
32
+ const result = spawnSync(
33
+ candidate.command,
34
+ [...candidate.args, "-c", PYTHON_VERSION_CHECK],
35
+ { stdio: "ignore", windowsHide: true }
36
+ );
37
+ return result.status === 0;
38
+ }
39
+
40
+ function findPython(platform = process.platform) {
41
+ for (const candidate of getCandidates(platform)) {
42
+ if (probeCandidate(candidate)) {
43
+ return candidate;
44
+ }
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function hasGlobCharacters(value) {
50
+ return value.includes("*") || value.includes("?") || value.includes("[");
51
+ }
52
+
53
+ function escapeRegex(value) {
54
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
55
+ }
56
+
57
+ function segmentToRegExp(segment) {
58
+ const pattern = escapeRegex(segment).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
59
+ return new RegExp(`^${pattern}$`);
60
+ }
61
+
62
+ function expandGlob(pattern) {
63
+ const pathLike = pattern.includes("/") || pattern.includes("\\") || path.isAbsolute(pattern);
64
+ if (!hasGlobCharacters(pattern) || !pathLike) {
65
+ return [pattern];
66
+ }
67
+
68
+ const absolute = path.isAbsolute(pattern);
69
+ const root = absolute ? path.parse(pattern).root : process.cwd();
70
+ const relativePattern = absolute ? path.relative(root, pattern) : pattern;
71
+ const segments = relativePattern.split(/[\\/]+/).filter(Boolean);
72
+ const matches = [];
73
+
74
+ const walk = (currentPath, index) => {
75
+ if (index >= segments.length) {
76
+ if (fs.existsSync(currentPath)) {
77
+ matches.push(currentPath);
78
+ }
79
+ return;
80
+ }
81
+
82
+ const segment = segments[index];
83
+ const nextIndex = index + 1;
84
+
85
+ if (!hasGlobCharacters(segment)) {
86
+ walk(path.join(currentPath, segment), nextIndex);
87
+ return;
88
+ }
89
+
90
+ if (!fs.existsSync(currentPath) || !fs.statSync(currentPath).isDirectory()) {
91
+ return;
92
+ }
93
+
94
+ const matcher = segmentToRegExp(segment);
95
+ for (const entry of fs.readdirSync(currentPath, { withFileTypes: true })) {
96
+ if (matcher.test(entry.name)) {
97
+ walk(path.join(currentPath, entry.name), nextIndex);
98
+ }
99
+ }
100
+ };
101
+
102
+ walk(root, 0);
103
+ return matches.length > 0 ? matches : [pattern];
104
+ }
105
+
106
+ function expandArgs(args) {
107
+ const expanded = [];
108
+ for (const arg of args) {
109
+ expanded.push(...expandGlob(arg));
110
+ }
111
+ return expanded;
112
+ }
113
+
114
+ function prepareEnv(env = process.env) {
115
+ const nextEnv = { ...env };
116
+ if (!nextEnv.PYTHONPYCACHEPREFIX) {
117
+ fs.mkdirSync(PYTHON_CACHE_PREFIX, { recursive: true });
118
+ nextEnv.PYTHONPYCACHEPREFIX = PYTHON_CACHE_PREFIX;
119
+ }
120
+ return nextEnv;
121
+ }
122
+
123
+ function runPython(args, options = {}) {
124
+ const platform = options.platform || process.platform;
125
+ const candidate = findPython(platform);
126
+
127
+ if (!candidate) {
128
+ const tried = getCandidates(platform).map((item) => item.label).join(", ");
129
+ console.error(`cdx: no compatible Python 3 interpreter found. Tried: ${tried}.`);
130
+ console.error("Install Python 3 and make one of those commands available on PATH.");
131
+ return 127;
132
+ }
133
+
134
+ const finalArgs = options.expandGlobs === false ? args.slice() : expandArgs(args);
135
+ const result = spawnSync(
136
+ candidate.command,
137
+ [...candidate.args, ...finalArgs],
138
+ {
139
+ env: prepareEnv(options.env),
140
+ stdio: "inherit",
141
+ windowsHide: true,
142
+ }
143
+ );
144
+
145
+ if (result.error) {
146
+ console.error(`cdx: failed to launch ${candidate.label}: ${result.error.message}`);
147
+ return 127;
148
+ }
149
+
150
+ if (result.signal) {
151
+ return SIGNAL_EXIT_CODES[result.signal] || 128;
152
+ }
153
+
154
+ return typeof result.status === "number" ? result.status : 1;
155
+ }
156
+
157
+ if (require.main === module) {
158
+ process.exit(runPython(process.argv.slice(2)));
159
+ }
160
+
161
+ module.exports = {
162
+ expandArgs,
163
+ findPython,
164
+ runPython,
165
+ };
@@ -0,0 +1,36 @@
1
+ # CHANGELOGS_0_4_0
2
+
3
+ Release date: 2026-04-16
4
+
5
+ ## CDX Manager 0.4.0
6
+
7
+ CDX Manager 0.4.0 adds portable session backup and restore, surfaces cached release-update notices inside the CLI, and tightens Codex status isolation across multiple accounts.
8
+
9
+ ### Portable session bundles
10
+
11
+ - Added `cdx export <file>` and `cdx import <file>` for moving sessions between machines.
12
+ - Added optional encrypted auth export with `--include-auth` and interactive or environment-driven passphrase handling.
13
+ - Added subset export/import support with `--sessions`.
14
+ - Preserved per-session state alongside session records so imported environments keep their local metadata.
15
+ - Added bundle schema validation and integrity checks during import.
16
+
17
+ ### Update awareness and installer hardening
18
+
19
+ - Added cached GitHub release checks so the CLI can warn when a newer `cdx-manager` release is available without hitting the network on every command.
20
+ - Surfaced update notices in interactive output and structured JSON warnings.
21
+ - Hardened the standalone install scripts to consume official release-archive checksums when available.
22
+ - Documented the checksum-backed installer flow and backup/restore usage in the README.
23
+
24
+ ### Status isolation fix
25
+
26
+ - Fixed Codex status parsing so boxed blank lines in TUI transcripts no longer drop the `Account:` context line.
27
+ - Restored account-aware status selection when multiple sessions contain similar `/status` blocks.
28
+ - Added regression coverage for mixed-account transcript selection and bundle export/import flows.
29
+
30
+ ### Validation
31
+
32
+ ```bash
33
+ npm run lint
34
+ npm test
35
+ python3 -m build --no-isolation
36
+ ```
@@ -0,0 +1,28 @@
1
+ # CHANGELOGS_0_4_1
2
+
3
+ Release date: 2026-04-19
4
+
5
+ ## CDX Manager 0.4.1
6
+
7
+ CDX Manager 0.4.1 fixes the Windows npm entrypoint so `cdx` no longer depends on `python3.exe` being present on PATH. It now ships a Node launcher that resolves a usable Python 3 interpreter cross-platform before invoking the existing Python CLI entrypoint.
8
+
9
+ ### Windows npm launcher fix
10
+
11
+ - Replaced the npm-facing `bin.cdx` target with a Node launcher at `bin/cdx.js`.
12
+ - Added Python discovery that tries `py -3`, then `python`, then `python3` on Windows.
13
+ - Kept the Python script under `bin/cdx` as the logical CLI entrypoint.
14
+ - Added a clear error message when no compatible Python 3 interpreter is available.
15
+
16
+ ### Documentation and packaging
17
+
18
+ - Updated the README with Windows Python prerequisites and the new launcher behavior.
19
+ - Added a portable shared Node wrapper for the npm test and lint scripts.
20
+ - Bumped the package version for both npm and PyPI release flows.
21
+
22
+ ### Validation
23
+
24
+ ```bash
25
+ npm run lint
26
+ npm test
27
+ node bin/cdx.js --version
28
+ ```
@@ -0,0 +1,13 @@
1
+ {
2
+ "schema_version": 1,
3
+ "releases": {
4
+ "v0.3.4": {
5
+ "github_tarball_sha256": "8e8111d6ec41b819fc0249800f175b5741cd11b1439c7e88a3feec770774b12d",
6
+ "github_zip_sha256": "e0bd79f731d86b83787e99b8f2220c8c5fbaa3d7507ebc52823aa5b8d11f0666"
7
+ },
8
+ "v0.4.0": {
9
+ "github_tarball_sha256": "47a99ff2663f4fee33339098d2cfe7c27fbd0f74a16a1f6711f3003287409ae8",
10
+ "github_zip_sha256": "ae7eff748e569c621ef203abb6b395fbf893370471e962252e8c781e7751e5c8"
11
+ }
12
+ }
13
+ }
package/install.ps1 CHANGED
@@ -1,11 +1,16 @@
1
1
  param(
2
2
  [string]$Version = $env:CDX_VERSION,
3
- [string]$Prefix = $env:CDX_PREFIX
3
+ [string]$Prefix = $env:CDX_PREFIX,
4
+ [string]$Sha256 = $env:CDX_SHA256,
5
+ [string]$ChecksumsUrl = $env:CDX_CHECKSUMS_URL
4
6
  )
5
7
 
6
8
  $ErrorActionPreference = "Stop"
7
9
 
8
10
  $repo = "AlexAgo83/cdx-manager"
11
+ if (-not $ChecksumsUrl) {
12
+ $ChecksumsUrl = "https://raw.githubusercontent.com/$repo/main/checksums/release-archives.json"
13
+ }
9
14
 
10
15
  function Require-Command {
11
16
  param([string]$Name)
@@ -44,6 +49,21 @@ New-Item -ItemType Directory -Force -Path $tmpRoot, $extractRoot, $binDir, $inst
44
49
 
45
50
  try {
46
51
  Invoke-WebRequest -Uri $archiveUrl -OutFile $archivePath
52
+ if (-not $Sha256) {
53
+ try {
54
+ $checksums = Invoke-RestMethod -Uri $ChecksumsUrl
55
+ $Sha256 = $checksums.releases.$tag.github_zip_sha256
56
+ } catch {
57
+ }
58
+ }
59
+ if ($Sha256) {
60
+ $actualSha256 = (Get-FileHash -Algorithm SHA256 -Path $archivePath).Hash.ToLowerInvariant()
61
+ if ($actualSha256 -ne $Sha256.ToLowerInvariant()) {
62
+ throw "cdx install: checksum mismatch for $tag`nexpected: $Sha256`nactual: $actualSha256"
63
+ }
64
+ } else {
65
+ Write-Warning "No official checksum available for $tag; continuing without verification."
66
+ }
47
67
  Expand-Archive -Path $archivePath -DestinationPath $extractRoot -Force
48
68
 
49
69
  $sourceDir = Get-ChildItem -Path $extractRoot -Directory | Select-Object -First 1
package/install.sh CHANGED
@@ -6,6 +6,7 @@ VERSION="${CDX_VERSION:-}"
6
6
  PREFIX="${PREFIX:-$HOME/.local}"
7
7
  BIN_DIR="${BIN_DIR:-$PREFIX/bin}"
8
8
  INSTALL_ROOT="${CDX_INSTALL_ROOT:-$PREFIX/share/cdx-manager}"
9
+ CHECKSUMS_URL="${CDX_CHECKSUMS_URL:-https://raw.githubusercontent.com/$REPO/main/checksums/release-archives.json}"
9
10
 
10
11
  need() {
11
12
  if ! command -v "$1" >/dev/null 2>&1; then
@@ -18,6 +19,38 @@ need curl
18
19
  need tar
19
20
  need python3
20
21
 
22
+ sha256_file() {
23
+ if command -v sha256sum >/dev/null 2>&1; then
24
+ sha256sum "$1" | awk '{print $1}'
25
+ return
26
+ fi
27
+ if command -v shasum >/dev/null 2>&1; then
28
+ shasum -a 256 "$1" | awk '{print $1}'
29
+ return
30
+ fi
31
+ echo "cdx install: missing checksum tool (sha256sum or shasum)" >&2
32
+ exit 1
33
+ }
34
+
35
+ resolve_expected_sha256() {
36
+ curl -fsSL "$CHECKSUMS_URL" |
37
+ python3 - "$1" <<'PY'
38
+ import json
39
+ import sys
40
+
41
+ tag = sys.argv[1]
42
+ try:
43
+ payload = json.load(sys.stdin)
44
+ except Exception:
45
+ raise SystemExit(1)
46
+
47
+ release = (payload.get("releases") or {}).get(tag) or {}
48
+ value = release.get("github_tarball_sha256")
49
+ if value:
50
+ print(value)
51
+ PY
52
+ }
53
+
21
54
  if [ -z "$VERSION" ]; then
22
55
  VERSION="$(
23
56
  curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" |
@@ -38,6 +71,24 @@ trap cleanup EXIT INT TERM
38
71
 
39
72
  ARCHIVE_URL="https://github.com/$REPO/archive/refs/tags/$TAG.tar.gz"
40
73
  curl -fsSL "$ARCHIVE_URL" -o "$TMP_DIR/cdx-manager.tar.gz"
74
+
75
+ EXPECTED_SHA256="${CDX_SHA256:-}"
76
+ if [ -z "$EXPECTED_SHA256" ]; then
77
+ EXPECTED_SHA256="$(resolve_expected_sha256 "$TAG" 2>/dev/null || true)"
78
+ fi
79
+
80
+ if [ -n "$EXPECTED_SHA256" ]; then
81
+ ACTUAL_SHA256="$(sha256_file "$TMP_DIR/cdx-manager.tar.gz")"
82
+ if [ "$ACTUAL_SHA256" != "$EXPECTED_SHA256" ]; then
83
+ echo "cdx install: checksum mismatch for $TAG" >&2
84
+ echo "expected: $EXPECTED_SHA256" >&2
85
+ echo "actual: $ACTUAL_SHA256" >&2
86
+ exit 1
87
+ fi
88
+ else
89
+ echo "cdx install: warning: no official checksum available for $TAG; continuing without verification" >&2
90
+ fi
91
+
41
92
  tar -xzf "$TMP_DIR/cdx-manager.tar.gz" -C "$TMP_DIR"
42
93
 
43
94
  SRC_DIR="$(find "$TMP_DIR" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.3.4",
3
+ "version": "0.4.1",
4
4
  "description": "Terminal session manager for Codex and Claude accounts.",
5
5
  "license": "MIT",
6
6
  "author": "Alexandre Agostini",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "files": [
27
27
  "bin",
28
+ "checksums",
28
29
  "changelogs",
29
30
  "src",
30
31
  "install.sh",
@@ -34,12 +35,12 @@
34
35
  "LICENSE"
35
36
  ],
36
37
  "bin": {
37
- "cdx": "bin/cdx"
38
+ "cdx": "bin/cdx.js"
38
39
  },
39
40
  "scripts": {
40
41
  "test": "npm run test:py",
41
- "test:py": "PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m unittest discover -s test -p 'test_*_py.py'",
42
- "lint": "PYTHONPYCACHEPREFIX=/tmp/pycache python3 -m py_compile bin/cdx src/*.py test/test_*_py.py",
42
+ "test:py": "node bin/python-runner.js -m unittest discover -s test -p test_*_py.py",
43
+ "lint": "node --check bin/cdx.js && node --check bin/python-runner.js && node bin/python-runner.js -m py_compile bin/cdx src/*.py test/test_*_py.py",
43
44
  "prepublishOnly": "npm run lint && npm test",
44
45
  "link": "npm link",
45
46
  "unlink": "npm unlink -g cdx-manager"
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.3.4"
7
+ version = "0.4.1"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"