cdx-manager 0.3.4 → 0.4.0

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.0-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
 
@@ -99,19 +102,24 @@ npm install -g cdx-manager
99
102
  With the standalone PowerShell installer:
100
103
 
101
104
  ```powershell
102
- irm https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.ps1 | iex
105
+ Invoke-WebRequest https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.ps1 -OutFile install.ps1
106
+ # Optional: set CDX_SHA256 before running if you have a trusted checksum
107
+ powershell -ExecutionPolicy Bypass -File .\install.ps1
103
108
  ```
104
109
 
105
110
  With the standalone GitHub installer:
106
111
 
107
112
  ```bash
108
- curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh | sh
113
+ curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
114
+ # Optional: set CDX_SHA256 before running if you have a trusted checksum
115
+ sh install.sh
109
116
  ```
110
117
 
111
118
  For a specific version:
112
119
 
113
120
  ```bash
114
- curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh | CDX_VERSION=v0.3.4 sh
121
+ curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
122
+ CDX_VERSION=v0.4.0 sh install.sh
115
123
  ```
116
124
 
117
125
  From source:
@@ -150,6 +158,13 @@ Alternatively, for a non-symlinked global source install:
150
158
  npm install -g .
151
159
  ```
152
160
 
161
+ Security note:
162
+
163
+ - The standalone installers try to resolve official release checksums from `checksums/release-archives.json`.
164
+ - You can still override verification explicitly through `CDX_SHA256`.
165
+ - Prefer `npm`, `pipx`, or `uv` when you want registry-backed install flows.
166
+ - If you use the standalone script, download it first, inspect it, and prefer a release with an official checksum entry.
167
+
153
168
  ### Environment
154
169
 
155
170
  By default, `cdx` stores all data under `~/.cdx/`. Override with:
@@ -220,11 +235,13 @@ cdx status
220
235
  | `cdx logout <name> [--json]` | Log out of a session |
221
236
  | `cdx rmv <name> [--force] [--json]` | Remove a session and its auth data (prompts for confirmation unless `--force`) |
222
237
  | `cdx clean [name] [--json]` | Clear launch transcript logs for one session or all sessions |
238
+ | `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 |
239
+ | `cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]` | Import sessions from a bundle into the current `CDX_HOME` |
223
240
  | `cdx doctor [--json]` | Inspect CLI dependencies, CDX_HOME permissions, missing state, orphan profiles, and pending quarantines |
224
241
  | `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 |
242
+ | `cdx notify <name> --at-reset [--poll seconds] [--once] [--json]` | Wait for a session reset time and send a desktop notification when due |
243
+ | `cdx notify --next-ready [--poll seconds] [--once] [--json]` | Wait until the recommended session is usable or needs a refresh after reset |
244
+ | `cdx status [--json] [--refresh]` | Show token usage table for all sessions; JSON returns a versioned payload with structured warnings |
228
245
  | `cdx status --small [--refresh]` / `cdx status -s [--refresh]` | Show compact token usage table without provider, blocking quota, credits, and updated columns |
229
246
  | `cdx status <name> [--json] [--refresh]` | Show detailed usage breakdown for one session |
230
247
  | `cdx --help` | Show usage |
@@ -246,6 +263,8 @@ Commands with machine-readable output:
246
263
  - `cdx ren ... --json`
247
264
  - `cdx rmv ... --json`
248
265
  - `cdx clean ... --json`
266
+ - `cdx export ... --json`
267
+ - `cdx import ... --json`
249
268
  - `cdx login ... --json`
250
269
  - `cdx logout ... --json`
251
270
  - `cdx doctor --json`
@@ -256,6 +275,7 @@ Success payloads follow a shared envelope:
256
275
 
257
276
  ```json
258
277
  {
278
+ "schema_version": 1,
259
279
  "ok": true,
260
280
  "action": "add",
261
281
  "message": "Created session work (codex)",
@@ -270,6 +290,7 @@ Errors use a shared stderr JSON envelope whenever `--json` is present:
270
290
 
271
291
  ```json
272
292
  {
293
+ "schema_version": 1,
273
294
  "ok": false,
274
295
  "error": {
275
296
  "code": "invalid_usage",
@@ -279,10 +300,39 @@ Errors use a shared stderr JSON envelope whenever `--json` is present:
279
300
  }
280
301
  ```
281
302
 
303
+ `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`.
304
+
282
305
  This makes `cdx-manager` usable from editor plugins, scripts, and desktop apps without scraping human-readable terminal output.
283
306
 
284
307
  ---
285
308
 
309
+ ## Backup And Restore
310
+
311
+ You can move sessions between machines with portable bundles:
312
+
313
+ ```bash
314
+ cdx export backup.cdx
315
+ cdx import backup.cdx
316
+ ```
317
+
318
+ To migrate auth and avoid logging in again, include auth data in an encrypted bundle:
319
+
320
+ ```bash
321
+ export CDX_BUNDLE_PASSPHRASE='choose-a-strong-passphrase'
322
+ cdx export backup-auth.cdx --include-auth --passphrase-env CDX_BUNDLE_PASSPHRASE
323
+ cdx import backup-auth.cdx --passphrase-env CDX_BUNDLE_PASSPHRASE
324
+ ```
325
+
326
+ Notes:
327
+
328
+ - `--include-auth` is encrypted and requires a passphrase.
329
+ - Without `--passphrase-env`, `cdx` prompts in an interactive terminal.
330
+ - `--sessions work,perso` exports or imports only a subset.
331
+ - `--force` allows overwriting existing destination sessions during import or replacing an existing bundle file during export.
332
+ - Auth bundles contain credentials. Treat them like secrets and delete them after transfer.
333
+
334
+ ---
335
+
286
336
  ## Available Scripts
287
337
 
288
338
  - `npm test`: run the Python test suite
@@ -321,6 +371,7 @@ src/
321
371
  cli.py # Top-level command router
322
372
  cli_commands.py # Command handlers and argument handling
323
373
  cli_render.py # Terminal formatting, tables, colors, and errors
374
+ backup_bundle.py # Portable session bundle encoding/decoding + auth encryption
324
375
  status_view.py # Status table/detail rendering and priority ranking
325
376
  provider_runtime.py # Provider launch/auth commands, transcripts, signals
326
377
  claude_refresh.py # Claude usage refresh orchestration
@@ -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,9 @@
1
+ {
2
+ "schema_version": 1,
3
+ "releases": {
4
+ "v0.3.4": {
5
+ "github_tarball_sha256": "8e8111d6ec41b819fc0249800f175b5741cd11b1439c7e88a3feec770774b12d",
6
+ "github_zip_sha256": "e0bd79f731d86b83787e99b8f2220c8c5fbaa3d7507ebc52823aa5b8d11f0666"
7
+ }
8
+ }
9
+ }
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.0",
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",
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.0"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,134 @@
1
+ import base64
2
+ import hashlib
3
+ import hmac
4
+ import json
5
+ import os
6
+ from datetime import datetime, timezone
7
+
8
+ from .errors import CdxError
9
+
10
+
11
+ BUNDLE_SCHEMA_VERSION = 1
12
+ _SALT_BYTES = 16
13
+ _NONCE_BYTES = 16
14
+ _SCRYPT_N = 2 ** 14
15
+ _SCRYPT_R = 8
16
+ _SCRYPT_P = 1
17
+ _PBKDF2_ITERATIONS = 200000
18
+
19
+
20
+ def _now_iso():
21
+ return datetime.now(timezone.utc).astimezone().isoformat()
22
+
23
+
24
+ def _b64_encode(data):
25
+ return base64.b64encode(data).decode("ascii")
26
+
27
+
28
+ def _b64_decode(data):
29
+ try:
30
+ return base64.b64decode(data.encode("ascii"))
31
+ except (AttributeError, ValueError, UnicodeEncodeError) as error:
32
+ raise CdxError("Bundle contains invalid base64 data.") from error
33
+
34
+
35
+ def read_bundle_meta(data):
36
+ try:
37
+ wrapper = json.loads(data.decode("utf-8"))
38
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
39
+ raise CdxError("Invalid bundle format.") from error
40
+
41
+ if wrapper.get("schema_version") != BUNDLE_SCHEMA_VERSION:
42
+ raise CdxError("Unsupported bundle schema version.")
43
+ return wrapper
44
+
45
+
46
+ def _derive_keys(passphrase, salt):
47
+ if not passphrase:
48
+ raise CdxError("A non-empty passphrase is required for bundles that include auth data.")
49
+ if isinstance(passphrase, str):
50
+ passphrase = passphrase.encode("utf-8")
51
+ if hasattr(hashlib, "scrypt"):
52
+ key_material = hashlib.scrypt(
53
+ passphrase,
54
+ salt=salt,
55
+ n=_SCRYPT_N,
56
+ r=_SCRYPT_R,
57
+ p=_SCRYPT_P,
58
+ dklen=64,
59
+ )
60
+ else:
61
+ key_material = hashlib.pbkdf2_hmac(
62
+ "sha256",
63
+ passphrase,
64
+ salt,
65
+ _PBKDF2_ITERATIONS,
66
+ dklen=64,
67
+ )
68
+ return key_material[:32], key_material[32:]
69
+
70
+
71
+ def _xor_keystream(data, key, nonce):
72
+ output = bytearray()
73
+ counter = 0
74
+ while len(output) < len(data):
75
+ block = hashlib.sha256(key + nonce + counter.to_bytes(8, "big")).digest()
76
+ output.extend(block)
77
+ counter += 1
78
+ return bytes(a ^ b for a, b in zip(data, output[:len(data)]))
79
+
80
+
81
+ def encode_bundle(payload, include_auth=False, passphrase=None):
82
+ payload_bytes = json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
83
+ wrapper = {
84
+ "schema_version": BUNDLE_SCHEMA_VERSION,
85
+ "bundle_version": 1,
86
+ "created_at": _now_iso(),
87
+ "include_auth": bool(include_auth),
88
+ "encrypted": bool(include_auth),
89
+ "session_names": [item["name"] for item in payload.get("sessions", [])],
90
+ }
91
+ if include_auth:
92
+ salt = os.urandom(_SALT_BYTES)
93
+ nonce = os.urandom(_NONCE_BYTES)
94
+ enc_key, mac_key = _derive_keys(passphrase, salt)
95
+ ciphertext = _xor_keystream(payload_bytes, enc_key, nonce)
96
+ mac = hmac.new(mac_key, nonce + ciphertext, hashlib.sha256).digest()
97
+ wrapper.update({
98
+ "salt": _b64_encode(salt),
99
+ "nonce": _b64_encode(nonce),
100
+ "hmac_sha256": _b64_encode(mac),
101
+ "payload": _b64_encode(ciphertext),
102
+ })
103
+ else:
104
+ wrapper["payload"] = _b64_encode(payload_bytes)
105
+ return json.dumps(wrapper, indent=2).encode("utf-8")
106
+
107
+
108
+ def decode_bundle(data, passphrase=None):
109
+ wrapper = read_bundle_meta(data)
110
+
111
+ encrypted = bool(wrapper.get("encrypted"))
112
+ payload_b64 = wrapper.get("payload")
113
+ if not isinstance(payload_b64, str):
114
+ raise CdxError("Bundle payload is missing.")
115
+
116
+ if encrypted:
117
+ salt = _b64_decode(wrapper.get("salt", ""))
118
+ nonce = _b64_decode(wrapper.get("nonce", ""))
119
+ expected_mac = _b64_decode(wrapper.get("hmac_sha256", ""))
120
+ ciphertext = _b64_decode(payload_b64)
121
+ enc_key, mac_key = _derive_keys(passphrase, salt)
122
+ actual_mac = hmac.new(mac_key, nonce + ciphertext, hashlib.sha256).digest()
123
+ if not hmac.compare_digest(actual_mac, expected_mac):
124
+ raise CdxError("Invalid bundle passphrase or corrupted bundle.")
125
+ payload_bytes = _xor_keystream(ciphertext, enc_key, nonce)
126
+ else:
127
+ payload_bytes = _b64_decode(payload_b64)
128
+
129
+ try:
130
+ payload = json.loads(payload_bytes.decode("utf-8"))
131
+ except (UnicodeDecodeError, json.JSONDecodeError) as error:
132
+ raise CdxError("Bundle payload is corrupt.") from error
133
+
134
+ return {"meta": wrapper, "payload": payload}
package/src/cli.py CHANGED
@@ -5,11 +5,14 @@ import os
5
5
  import sys
6
6
 
7
7
  from .cli_commands import (
8
+ API_SCHEMA_VERSION,
8
9
  STATUS_USAGE,
9
10
  handle_add,
10
11
  handle_clean,
11
12
  handle_copy,
12
13
  handle_doctor,
14
+ handle_export,
15
+ handle_import,
13
16
  handle_launch,
14
17
  handle_login,
15
18
  handle_logout,
@@ -39,8 +42,9 @@ from .status_view import (
39
42
  _format_status_detail,
40
43
  _format_status_rows,
41
44
  )
45
+ from .update_check import check_for_update
42
46
 
43
- VERSION = "0.3.4"
47
+ VERSION = "0.4.0"
44
48
 
45
49
 
46
50
  # ---------------------------------------------------------------------------
@@ -64,6 +68,8 @@ def _print_help(use_color=False):
64
68
  f" {_style('cdx logout <name> [--json]', '36', use_color)}",
65
69
  f" {_style('cdx rmv <name> [--force] [--json]', '36', use_color)}",
66
70
  f" {_style('cdx clean [name] [--json]', '36', use_color)}",
71
+ f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
72
+ f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
67
73
  f" {_style('cdx doctor [--json]', '36', use_color)}",
68
74
  f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
69
75
  f" {_style('cdx notify <name> --at-reset [--json]', '36', use_color)}",
@@ -96,6 +102,7 @@ def format_json_error(error):
96
102
  elif "requires an interactive terminal" in message or "requires confirmation" in message:
97
103
  code = "interactive_terminal_required"
98
104
  return json.dumps({
105
+ "schema_version": API_SCHEMA_VERSION,
99
106
  "ok": False,
100
107
  "error": {
101
108
  "code": code,
@@ -105,6 +112,35 @@ def format_json_error(error):
105
112
  }, indent=2)
106
113
 
107
114
 
115
+ def _get_update_notice(service, env, options):
116
+ checker = options.get("checkForUpdate") or check_for_update
117
+ return checker(
118
+ service["base_dir"],
119
+ VERSION,
120
+ env=env,
121
+ now_fn=options.get("now"),
122
+ )
123
+
124
+
125
+ def _update_warning_payload(notice):
126
+ if not notice:
127
+ return []
128
+ message = f"Update available: cdx-manager {notice['latest_version']} (current {VERSION})"
129
+ return [{
130
+ "code": "update_available",
131
+ "message": message,
132
+ "latest_version": notice["latest_version"],
133
+ "url": notice.get("url"),
134
+ }]
135
+
136
+
137
+ def _update_warning_text(notice):
138
+ if not notice:
139
+ return None
140
+ suffix = f" {notice['url']}" if notice.get("url") else ""
141
+ return f"Update available: cdx-manager {notice['latest_version']} (current {VERSION}).{suffix}"
142
+
143
+
108
144
  # ---------------------------------------------------------------------------
109
145
  # main()
110
146
  # ---------------------------------------------------------------------------
@@ -145,11 +181,15 @@ def main(argv, options=None):
145
181
 
146
182
  if argv == ["--json"]:
147
183
  rows = service["format_list_rows"]()
148
- out(f"{json.dumps(_list_json_payload(rows), indent=2)}\n")
184
+ notice = _get_update_notice(service, env, options)
185
+ out(f"{json.dumps(_list_json_payload(rows, notice=notice), indent=2)}\n")
149
186
  return 0
150
187
 
151
188
  if not argv:
189
+ notice = _get_update_notice(service, env, options)
152
190
  out(f"{_format_sessions(service, use_color=use_color)}\n")
191
+ if notice:
192
+ out(f"{_style(_update_warning_text(notice), '33', use_color)}\n")
153
193
  return 0
154
194
 
155
195
  command, *rest = argv
@@ -165,6 +205,9 @@ def main(argv, options=None):
165
205
  "spawn": spawn,
166
206
  "spawn_sync": spawn_sync,
167
207
  "stdin_is_tty": stdin_is_tty,
208
+ "update_notice": _get_update_notice(service, env, options) if command not in (
209
+ "add", "cp", "ren", "rename", "mv", "rmv", "clean", "doctor", "repair", "notify", "status", "login", "logout", "export", "import", "help", "version"
210
+ ) else None,
168
211
  "use_color": use_color,
169
212
  }
170
213
 
@@ -183,6 +226,12 @@ def main(argv, options=None):
183
226
  if command == "clean":
184
227
  return handle_clean(rest, ctx)
185
228
 
229
+ if command == "export":
230
+ return handle_export(rest, ctx)
231
+
232
+ if command == "import":
233
+ return handle_import(rest, ctx)
234
+
186
235
  if command == "doctor":
187
236
  return handle_doctor(rest, ctx)
188
237
 
@@ -215,12 +264,13 @@ def main(argv, options=None):
215
264
  raise CdxError(f"Unknown command: {command}. Use cdx --help.")
216
265
 
217
266
 
218
- def _list_json_payload(rows):
267
+ def _list_json_payload(rows, notice=None):
219
268
  return {
269
+ "schema_version": API_SCHEMA_VERSION,
220
270
  "ok": True,
221
271
  "action": "list",
222
272
  "message": "Listed known sessions",
223
- "warnings": [],
273
+ "warnings": _update_warning_payload(notice),
224
274
  "sessions": rows,
225
275
  }
226
276