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 +58 -7
- package/changelogs/CHANGELOGS_0_4_0.md +36 -0
- package/checksums/release-archives.json +9 -0
- package/install.ps1 +21 -1
- package/install.sh +51 -0
- package/package.json +2 -1
- package/pyproject.toml +1 -1
- package/src/backup_bundle.py +134 -0
- package/src/cli.py +54 -4
- package/src/cli_commands.py +242 -13
- package/src/session_service.py +185 -2
- package/src/session_store.py +11 -0
- package/src/status_source.py +3 -1
- package/src/update_check.py +107 -0
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
+
[](LICENSE)  
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
+
```
|
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
|
+
"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
|
@@ -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.
|
|
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
|
-
|
|
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
|
|