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 +3 -2
- package/changelogs/CHANGELOGS_0_4_1.md +5 -5
- package/changelogs/CHANGELOGS_0_4_2.md +32 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +1 -1
- package/src/provider_runtime.py +44 -17
- package/src/session_service.py +17 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CDX Manager
|
|
2
2
|
|
|
3
|
-
[](LICENSE) ](LICENSE)  
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
20
|
-
- Bumped the package
|
|
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
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
package/src/provider_runtime.py
CHANGED
|
@@ -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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
[
|
|
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
|
|
package/src/session_service.py
CHANGED
|
@@ -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,
|