cdx-manager 0.4.1 → 0.4.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.4.1-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.4.2-4C8BF5) ![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?logo=python&logoColor=white)
4
4
 
5
5
  **Run multiple Codex and Claude sessions from one terminal. Switch between accounts instantly.**
6
6
 
@@ -48,6 +48,7 @@ One command to launch any session. Zero auth juggling.
48
48
  - Environment isolation per session:
49
49
  - Codex sessions override `CODEX_HOME` to a dedicated profile directory.
50
50
  - Claude sessions override `HOME` to a dedicated profile directory.
51
+ - New Codex sessions seed their auth home from your existing global `~/.codex/auth.json` when available, so an already logged-in Codex CLI can be reused without giving up per-session isolation afterward.
51
52
  - Persistence:
52
53
  - Session registry at `~/.cdx/sessions.json` (versioned JSON store).
53
54
  - Per-session state at `~/.cdx/state/<name>.json`.
@@ -121,7 +122,7 @@ For a specific version:
121
122
 
122
123
  ```bash
123
124
  curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh -o install.sh
124
- CDX_VERSION=v0.4.1 sh install.sh
125
+ CDX_VERSION=v0.4.2 sh install.sh
125
126
  ```
126
127
 
127
128
  From source:
@@ -4,20 +4,20 @@ Release date: 2026-04-19
4
4
 
5
5
  ## CDX Manager 0.4.1
6
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.
7
+ CDX Manager 0.4.1 fixes the Windows npm entry point 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 entry point.
8
8
 
9
- ### Windows npm launcher fix
9
+ ### Windows npm launcher
10
10
 
11
11
  - Replaced the npm-facing `bin.cdx` target with a Node launcher at `bin/cdx.js`.
12
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.
13
+ - Kept the existing Python script under `bin/cdx` as the primary CLI entry point.
14
14
  - Added a clear error message when no compatible Python 3 interpreter is available.
15
15
 
16
16
  ### Documentation and packaging
17
17
 
18
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.
19
+ - Added a shared portable Node wrapper for the npm test and lint scripts.
20
+ - Bumped the package versions for the npm and PyPI release workflows.
21
21
 
22
22
  ### Validation
23
23
 
@@ -0,0 +1,32 @@
1
+ # CHANGELOGS_0_4_2
2
+
3
+ Release date: 2026-04-19
4
+
5
+ ## CDX Manager 0.4.2
6
+
7
+ CDX Manager 0.4.2 fixes Codex session bootstrap on Windows so `cdx add` can reuse an existing logged-in Codex CLI without requiring a second manual login flow. It also hardens executable resolution for the Codex probe so Windows shell shims and direct process spawning behave consistently.
8
+
9
+ ### Codex auth bootstrap
10
+
11
+ - Seeded new Codex session auth homes from the global `~/.codex/auth.json` when available.
12
+ - Short-circuited the Codex auth probe when a session already has an auth file in its isolated home.
13
+ - Kept the per-session auth directory model intact after bootstrap so session isolation still applies.
14
+
15
+ ### Windows command resolution
16
+
17
+ - Resolved `codex` through the active `PATH` before invoking the login-status probe.
18
+ - Applied the same command-resolution path to interactive provider launches when the default process spawner is used.
19
+ - Added regression coverage for Codex auth bootstrap and resolved-command spawning on Windows.
20
+
21
+ ### Documentation
22
+
23
+ - Documented the Codex auth bootstrap behavior in the README.
24
+
25
+ ### Validation
26
+
27
+ ```bash
28
+ npm run lint
29
+ npm test
30
+ node bin/cdx.js rmv main --force
31
+ node bin/cdx.js add main
32
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cdx-manager",
3
- "version": "0.4.1",
3
+ "version": "0.4.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.4.1"
7
+ version = "0.4.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
@@ -44,7 +44,7 @@ from .status_view import (
44
44
  )
45
45
  from .update_check import check_for_update
46
46
 
47
- VERSION = "0.4.1"
47
+ VERSION = "0.4.2"
48
48
 
49
49
 
50
50
  # ---------------------------------------------------------------------------
@@ -2,6 +2,7 @@ import json
2
2
  import os
3
3
  import signal
4
4
  import shlex
5
+ import shutil
5
6
  import subprocess
6
7
  import sys
7
8
  from datetime import datetime, timezone
@@ -157,26 +158,49 @@ def _build_auth_action_spec(session, action, cwd=None, env_override=None):
157
158
  "options": {"cwd": cwd, "env": env}, "label": f"codex {action}"}
158
159
 
159
160
 
161
+ def _format_probe_failure(session, spec, error):
162
+ command = spec["command"]
163
+ if isinstance(error, FileNotFoundError):
164
+ return CdxError(
165
+ f"Failed to check login status for {session['name']}: {command} CLI not found on PATH. "
166
+ f"Install {command} and retry cdx add {session['name']}.",
167
+ 127,
168
+ )
169
+ message = getattr(error, "message", None) or str(error)
170
+ return CdxError(f"Failed to check login status for {session['name']}: {message}")
171
+
172
+
173
+ def _resolve_command(command, env=None):
174
+ env = env or os.environ
175
+ return shutil.which(command, path=env.get("PATH")) or command
176
+
177
+
160
178
  def _probe_provider_auth(session, spawn_sync=None, env_override=None):
161
179
  spawn_sync = spawn_sync or subprocess.run
162
180
  spec = _build_login_status_spec(session, env_override)
163
- if spawn_sync is subprocess.run:
164
- result = subprocess.run(
165
- [spec["command"]] + spec["args"],
166
- env=spec["env"],
167
- capture_output=True, text=True,
168
- )
169
- output = (result.stdout or "") + (result.stderr or "")
170
- else:
171
- result = spawn_sync(spec["command"], spec["args"], spec)
172
- error = result.get("error") if isinstance(result, dict) else getattr(result, "error", None)
173
- if error:
174
- raise CdxError(
175
- f"Failed to check login status for {session['name']}: {getattr(error, 'message', str(error))}"
181
+ if session.get("provider") == "codex":
182
+ auth_path = os.path.join(_get_auth_home(session), "auth.json")
183
+ if os.path.isfile(auth_path):
184
+ return True
185
+ try:
186
+ if spawn_sync is subprocess.run:
187
+ command = _resolve_command(spec["command"], spec["env"])
188
+ result = subprocess.run(
189
+ [command] + spec["args"],
190
+ env=spec["env"],
191
+ capture_output=True, text=True,
176
192
  )
177
- stdout = result.get("stdout") if isinstance(result, dict) else getattr(result, "stdout", "")
178
- stderr = result.get("stderr") if isinstance(result, dict) else getattr(result, "stderr", "")
179
- output = (stdout or "") + (stderr or "")
193
+ output = (result.stdout or "") + (result.stderr or "")
194
+ else:
195
+ result = spawn_sync(spec["command"], spec["args"], spec)
196
+ error = result.get("error") if isinstance(result, dict) else getattr(result, "error", None)
197
+ if error:
198
+ raise _format_probe_failure(session, spec, error)
199
+ stdout = result.get("stdout") if isinstance(result, dict) else getattr(result, "stdout", "")
200
+ stderr = result.get("stderr") if isinstance(result, dict) else getattr(result, "stderr", "")
201
+ output = (stdout or "") + (stderr or "")
202
+ except FileNotFoundError as error:
203
+ raise _format_probe_failure(session, spec, error)
180
204
  return spec["parser"](output)
181
205
 
182
206
 
@@ -205,8 +229,11 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
205
229
  else _build_auth_action_spec(session, action, cwd=cwd, env_override=env_override)
206
230
  )
207
231
  def start_child(current_spec):
232
+ command = current_spec["command"]
233
+ if spawn is subprocess.Popen:
234
+ command = _resolve_command(command, current_spec.get("options", {}).get("env"))
208
235
  return spawn(
209
- [current_spec["command"]] + current_spec["args"],
236
+ [command] + current_spec["args"],
210
237
  **{k: v for k, v in current_spec.get("options", {}).items() if k != "stdio"},
211
238
  )
212
239
 
@@ -55,6 +55,21 @@ def _ensure_private_dir(path):
55
55
  pass
56
56
 
57
57
 
58
+ def _get_global_codex_home(env=None):
59
+ env = env or os.environ
60
+ return env.get("CODEX_HOME") or os.path.join(os.path.expanduser("~"), ".codex")
61
+
62
+
63
+ def _seed_codex_auth_from_global(auth_home, env=None):
64
+ source_home = _get_global_codex_home(env)
65
+ source_auth = os.path.join(source_home, "auth.json")
66
+ dest_auth = os.path.join(auth_home, "auth.json")
67
+ if source_home == auth_home or os.path.exists(dest_auth) or not os.path.isfile(source_auth):
68
+ return False
69
+ shutil.copy2(source_auth, dest_auth)
70
+ return True
71
+
72
+
58
73
  def _local_now_iso():
59
74
  return datetime.now().astimezone().isoformat()
60
75
 
@@ -303,6 +318,8 @@ def create_session_service(options=None):
303
318
  _ensure_private_dir(os.path.join(base_dir, "profiles"))
304
319
  _ensure_private_dir(session_root)
305
320
  _ensure_private_dir(auth_home)
321
+ if normalized_provider == "codex":
322
+ _seed_codex_auth_from_global(auth_home, env=env)
306
323
  now = _local_now_iso()
307
324
  session = {
308
325
  "name": name,