cdx-manager 0.9.1 → 0.9.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.
@@ -0,0 +1,33 @@
1
+ # CDX Manager 0.9.2
2
+
3
+ ## Highlights
4
+
5
+ - `cdx import` gains a `--merge` flag to fill in missing data without overwriting what already exists locally.
6
+ - npm and PyPI publication workflows now verify that the release tag's commit has a successful `CI` run before any registry upload step.
7
+
8
+ ## Changes
9
+
10
+ ### cdx import: `--merge` mode
11
+
12
+ `cdx import` previously offered two stances on existing sessions: reject the conflict (default) or erase and replace (`--force`). A third mode is now available:
13
+
14
+ ```
15
+ cdx import backup.cdx --merge
16
+ ```
17
+
18
+ With `--merge`, for each session that already exists locally:
19
+
20
+ - **Session fields** — existing values are kept; fields absent locally are pulled in from the bundle.
21
+ - **Session state** — same merge rule: local data wins, bundle fills the gaps.
22
+ - **Profile files** (auth.json, credentials, etc.) — files that already exist on disk are left untouched; files missing locally are restored from the bundle.
23
+ - **New sessions** in the bundle that have no local counterpart are imported normally.
24
+
25
+ `--force` and `--merge` are mutually exclusive and raise a clear error if combined.
26
+
27
+ ## Validation
28
+
29
+ - `npm run prepublishOnly`
30
+ - `npm pack --dry-run`
31
+ - `python -m unittest discover -s test -p 'test_release_ci_py.py'`
32
+ - `logics-manager lint --require-status`
33
+ - `logics-manager audit --legacy-cutoff-version 1.1.0 --group-by-doc`
@@ -68,6 +68,10 @@
68
68
  "v0.9.0": {
69
69
  "github_tarball_sha256": "809af5746e1287f4c5dc7a2fb7583e67e212aa250c45be905c32374e5dee26d6",
70
70
  "github_zip_sha256": "9118f81645c00a4a660923a1ae290f48bd458af7ebfcf9f6580a3b91023b7c76"
71
+ },
72
+ "v0.9.1": {
73
+ "github_tarball_sha256": "8cbab620c823aeaf56e72c1a4d20e251a1c7142bfeeb3d516321d59c2c0a621d",
74
+ "github_zip_sha256": "253909ede6dca61937835bd4ee29145ddacaa08fbdfba1b39810da73ae2ccc84"
71
75
  }
72
76
  }
73
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.9.1",
3
+ "version": "0.9.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.9.1"
7
+ version = "0.9.2"
8
8
  description = "Terminal session manager for Codex and Claude accounts."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
package/src/cli.py CHANGED
@@ -64,7 +64,7 @@ from .status_view import (
64
64
  )
65
65
  from .update_check import check_for_update, check_logics_manager_for_update
66
66
 
67
- VERSION = "0.9.1"
67
+ VERSION = "0.9.2"
68
68
 
69
69
 
70
70
  # ---------------------------------------------------------------------------
@@ -108,7 +108,7 @@ def _print_help(use_color=False):
108
108
  f" {_style('cdx rmv <name> [--force] [--json]', '36', use_color)}",
109
109
  f" {_style('cdx clean [name] [--json]', '36', use_color)}",
110
110
  f" {_style('cdx export <file> [--include-auth] [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
111
- f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force] [--json]', '36', use_color)}",
111
+ f" {_style('cdx import <file> [--sessions a,b] [--passphrase-env VAR] [--force|--merge] [--json]', '36', use_color)}",
112
112
  f" {_style('cdx doctor [--json]', '36', use_color)}",
113
113
  f" {_style('cdx repair [--dry-run] [--force] [--json]', '36', use_color)}",
114
114
  f" {_style('cdx view [--json]', '36', use_color)}",
@@ -55,7 +55,7 @@ DOCTOR_USAGE = "Usage: cdx doctor [--json]"
55
55
  REPAIR_USAGE = "Usage: cdx repair [--dry-run] [--force] [--json]"
56
56
  UPDATE_USAGE = "Usage: cdx update [--check] [--yes] [--json] [--version TAG]"
57
57
  EXPORT_USAGE = "Usage: cdx export <file> [--include-auth] [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
58
- IMPORT_USAGE = "Usage: cdx import <file> [--force] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
58
+ IMPORT_USAGE = "Usage: cdx import <file> [--force|--merge] [--json] [--sessions name1,name2] [--passphrase-env VAR]"
59
59
  CONTEXT_USAGE = "Usage: cdx context show|path|init|edit|clear|set [text...] [--json]"
60
60
  HANDOFF_USAGE = "Usage: cdx handoff <name> [--json] | cdx handoff <source> <target> [--json]"
61
61
  SET_USAGE = "Usage: cdx set <name>|--sessions all|a,b|--provider PROVIDER [--power low|medium|high|xhigh|max] [--permission review|default|auto|full] [--fast on|off] [--rtk on|off] [--logics on|off] [--model MODEL] [--priority 0..100] [--json]"
@@ -1000,6 +1000,7 @@ def _parse_export_args(args):
1000
1000
  def _parse_import_args(args):
1001
1001
  parsed = _parse_flag_args(args, {
1002
1002
  "--force": {"key": "force", "type": "bool", "default": False},
1003
+ "--merge": {"key": "merge", "type": "bool", "default": False},
1003
1004
  "--json": {"key": "json", "type": "bool", "default": False},
1004
1005
  "--sessions": {
1005
1006
  "key": "session_names",
@@ -1012,6 +1013,8 @@ def _parse_import_args(args):
1012
1013
  parsed["file_path"] = parsed.pop("positionals")[0] if parsed["positionals"] else None
1013
1014
  if not parsed["file_path"]:
1014
1015
  raise CdxError(IMPORT_USAGE)
1016
+ if parsed["force"] and parsed["merge"]:
1017
+ raise CdxError("--force and --merge are mutually exclusive.")
1015
1018
  return parsed
1016
1019
 
1017
1020
 
@@ -2643,6 +2646,7 @@ def handle_import(rest, ctx):
2643
2646
  passphrase=passphrase,
2644
2647
  session_names=parsed["session_names"],
2645
2648
  force=parsed["force"],
2649
+ merge=parsed["merge"],
2646
2650
  )
2647
2651
  session_count = len(result["session_names"])
2648
2652
  auth_suffix = " with auth" if result["include_auth"] else ""
@@ -1173,7 +1173,7 @@ def create_session_service(options=None):
1173
1173
  "bundle_size_bytes": len(bundle_bytes),
1174
1174
  }
1175
1175
 
1176
- def import_bundle(file_path, passphrase=None, session_names=None, force=False):
1176
+ def import_bundle(file_path, passphrase=None, session_names=None, force=False, merge=False):
1177
1177
  if not file_path or not os.path.isfile(file_path):
1178
1178
  raise CdxError(f"Bundle file not found: {file_path}")
1179
1179
  with open(file_path, "rb") as handle:
@@ -1193,15 +1193,18 @@ def create_session_service(options=None):
1193
1193
 
1194
1194
  existing = {session["name"] for session in list_sessions()}
1195
1195
  conflicts = [name for name in names if name in existing]
1196
- if conflicts and not force:
1196
+ if conflicts and not force and not merge:
1197
1197
  raise CdxError(f"Import would overwrite existing sessions: {', '.join(conflicts)}")
1198
1198
 
1199
1199
  for session_payload in imported_sessions:
1200
1200
  name = session_payload["name"]
1201
1201
  _validate_new_session_name(name)
1202
1202
  provider = _normalize_provider(session_payload["provider"])
1203
- if name in existing:
1203
+ is_existing = name in existing
1204
+
1205
+ if is_existing and force:
1204
1206
  remove_session(name)
1207
+ is_existing = False
1205
1208
 
1206
1209
  session_root = _get_session_root(name)
1207
1210
  auth_home = _get_session_auth_home(name, provider)
@@ -1210,18 +1213,38 @@ def create_session_service(options=None):
1210
1213
  _ensure_private_dir(session_root)
1211
1214
  _ensure_private_dir(auth_home)
1212
1215
 
1213
- session_record = {
1214
- **session_payload,
1215
- "provider": provider,
1216
- "enabled": session_payload.get("enabled", True) is not False,
1217
- "sessionRoot": session_root,
1218
- "authHome": auth_home,
1219
- }
1220
- store["replace_session"](name, session_record)
1216
+ if is_existing and merge:
1217
+ existing_record = store["get_session"](name) or {}
1218
+ bundle_record = {
1219
+ **session_payload,
1220
+ "provider": provider,
1221
+ "enabled": session_payload.get("enabled", True) is not False,
1222
+ "sessionRoot": session_root,
1223
+ "authHome": auth_home,
1224
+ }
1225
+ # Existing values take precedence; bundle fills in missing keys only.
1226
+ merged_record = {**bundle_record, **{k: v for k, v in existing_record.items() if v is not None}}
1227
+ merged_record["sessionRoot"] = session_root
1228
+ merged_record["authHome"] = auth_home
1229
+ store["replace_session"](name, merged_record)
1230
+ else:
1231
+ session_record = {
1232
+ **session_payload,
1233
+ "provider": provider,
1234
+ "enabled": session_payload.get("enabled", True) is not False,
1235
+ "sessionRoot": session_root,
1236
+ "authHome": auth_home,
1237
+ }
1238
+ store["replace_session"](name, session_record)
1221
1239
 
1222
1240
  state = (payload.get("states") or {}).get(name)
1223
1241
  if state is not None:
1224
- store["write_session_state"](name, state)
1242
+ if is_existing and merge:
1243
+ existing_state = store["read_session_state"](name) or {}
1244
+ merged_state = {**state, **{k: v for k, v in existing_state.items() if v is not None}}
1245
+ store["write_session_state"](name, merged_state)
1246
+ else:
1247
+ store["write_session_state"](name, state)
1225
1248
 
1226
1249
  for item in (payload.get("profiles") or {}).get(name, []):
1227
1250
  rel_path = _safe_relpath(item.get("path"))
@@ -1230,6 +1253,9 @@ def create_session_service(options=None):
1230
1253
  except (AttributeError, ValueError, UnicodeEncodeError) as error:
1231
1254
  raise CdxError(f"Bundle contains invalid file data for session {name}: {rel_path}") from error
1232
1255
  dest_path = os.path.join(session_root, rel_path)
1256
+ # In merge mode, skip files that already exist locally.
1257
+ if is_existing and merge and os.path.exists(dest_path):
1258
+ continue
1233
1259
  _ensure_private_dir(os.path.dirname(dest_path))
1234
1260
  with open(dest_path, "wb") as handle:
1235
1261
  handle.write(content)