cdx-manager 0.3.2 → 0.3.3
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 +2 -2
- package/changelogs/CHANGELOGS_0_3_3.md +27 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/cli.py +29 -1
- package/src/notify.py +26 -1
- package/src/provider_runtime.py +27 -5
- package/src/session_store.py +30 -13
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ If you use AI coding tools at scale ; multiple accounts, multiple providers : yo
|
|
|
6
6
|
|
|
7
7
|
One command to launch any session. Zero auth juggling.
|
|
8
8
|
|
|
9
|
-
[](LICENSE) ](LICENSE)  
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -96,7 +96,7 @@ curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.
|
|
|
96
96
|
For a specific version:
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh | CDX_VERSION=v0.3.
|
|
99
|
+
curl -fsSL https://raw.githubusercontent.com/AlexAgo83/cdx-manager/main/install.sh | CDX_VERSION=v0.3.3 sh
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
From source:
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# CHANGELOGS_0_3_3
|
|
2
|
+
|
|
3
|
+
Release date: 2026-04-16
|
|
4
|
+
|
|
5
|
+
## CDX Manager 0.3.3
|
|
6
|
+
|
|
7
|
+
CDX Manager 0.3.3 adds Windows compatibility across the full codebase.
|
|
8
|
+
|
|
9
|
+
### Windows support
|
|
10
|
+
|
|
11
|
+
- **Session store locking**: replaced `fcntl.flock` (Unix-only) with `msvcrt.locking` on Windows, with `seek(0)` to ensure consistent byte-range locking.
|
|
12
|
+
- **Signal handling**: guarded `signal.SIGHUP` references behind `hasattr` checks — `SIGHUP` does not exist on Windows.
|
|
13
|
+
- **Profile isolation**: added `_home_env_overrides()` helper that sets `USERPROFILE`, `HOMEDRIVE`, and `HOMEPATH` in addition to `HOME` when launching the `claude` CLI on Windows, so Node.js `os.homedir()` resolves to the correct session profile.
|
|
14
|
+
- **Desktop notifications**: `cdx notify` now sends a notification via PowerShell `System.Windows.Forms.MessageBox` on Windows (falls back silently if PowerShell is unavailable).
|
|
15
|
+
- **ANSI colors**: `cli_entry` enables VT processing via `ctypes.windll.kernel32.SetConsoleMode` on Windows so color output works in terminals that support it.
|
|
16
|
+
- **Console encoding**: `cli_entry` reconfigures `stdout`/`stderr` to UTF-8 on Windows to prevent `UnicodeEncodeError` on non-ASCII session names.
|
|
17
|
+
|
|
18
|
+
### Maintenance
|
|
19
|
+
|
|
20
|
+
- Expanded `.gitignore` with standard Python build artifacts (`__pycache__/`, `*.egg-info/`, `dist/`, `build/`), virtual environments, coverage output, and OS-specific files (`.DS_Store`, `Thumbs.db`, `desktop.ini`).
|
|
21
|
+
|
|
22
|
+
### Validation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm run lint
|
|
26
|
+
npm test
|
|
27
|
+
```
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
package/src/cli.py
CHANGED
|
@@ -39,7 +39,7 @@ from .status_view import (
|
|
|
39
39
|
_format_status_rows,
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
VERSION = "0.3.
|
|
42
|
+
VERSION = "0.3.3"
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
# ---------------------------------------------------------------------------
|
|
@@ -180,7 +180,35 @@ def main(argv, options=None):
|
|
|
180
180
|
raise CdxError(f"Unknown command: {command}. Use cdx --help.")
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
def _enable_windows_ansi():
|
|
184
|
+
if sys.platform != "win32":
|
|
185
|
+
return
|
|
186
|
+
try:
|
|
187
|
+
import ctypes
|
|
188
|
+
kernel32 = ctypes.windll.kernel32
|
|
189
|
+
for handle_id in (-10, -11, -12): # stdin, stdout, stderr
|
|
190
|
+
handle = kernel32.GetStdHandle(handle_id)
|
|
191
|
+
mode = ctypes.c_ulong()
|
|
192
|
+
if kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
193
|
+
kernel32.SetConsoleMode(handle, mode.value | 0x0004)
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _configure_windows_encoding():
|
|
199
|
+
if sys.platform != "win32":
|
|
200
|
+
return
|
|
201
|
+
for stream in (sys.stdout, sys.stderr):
|
|
202
|
+
try:
|
|
203
|
+
if hasattr(stream, "reconfigure"):
|
|
204
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
|
|
183
209
|
def cli_entry():
|
|
210
|
+
_enable_windows_ansi()
|
|
211
|
+
_configure_windows_encoding()
|
|
184
212
|
try:
|
|
185
213
|
raise SystemExit(main(sys.argv[1:]))
|
|
186
214
|
except CdxError as error:
|
package/src/notify.py
CHANGED
|
@@ -114,13 +114,38 @@ def _event(ready, title, message, session_name, target_timestamp=None):
|
|
|
114
114
|
|
|
115
115
|
|
|
116
116
|
def send_desktop_notification(title, message, spawn_sync=None, env=None):
|
|
117
|
+
import sys
|
|
117
118
|
spawn_sync = spawn_sync or subprocess.run
|
|
118
119
|
env = env or os.environ
|
|
119
|
-
if
|
|
120
|
+
if sys.platform == "win32":
|
|
121
|
+
_send_windows_notification(title, message, spawn_sync, env)
|
|
122
|
+
elif shutil_which("osascript", env):
|
|
120
123
|
script = f'display notification "{_escape_applescript(message)}" with title "{_escape_applescript(title)}"'
|
|
121
124
|
spawn_sync(["osascript", "-e", script], env=env, capture_output=True, text=True)
|
|
122
125
|
|
|
123
126
|
|
|
127
|
+
def _send_windows_notification(title, message, spawn_sync, env):
|
|
128
|
+
title_escaped = _escape_powershell(title)
|
|
129
|
+
message_escaped = _escape_powershell(message)
|
|
130
|
+
script = (
|
|
131
|
+
"Add-Type -AssemblyName System.Windows.Forms; "
|
|
132
|
+
f"[System.Windows.Forms.MessageBox]::Show('{message_escaped}', '{title_escaped}')"
|
|
133
|
+
)
|
|
134
|
+
try:
|
|
135
|
+
spawn_sync(
|
|
136
|
+
["powershell", "-NoProfile", "-NonInteractive", "-Command", script],
|
|
137
|
+
env=env,
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
)
|
|
141
|
+
except (FileNotFoundError, OSError):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _escape_powershell(value):
|
|
146
|
+
return str(value).replace("'", "''")
|
|
147
|
+
|
|
148
|
+
|
|
124
149
|
def shutil_which(command, env):
|
|
125
150
|
import shutil
|
|
126
151
|
return shutil.which(command, path=env.get("PATH"))
|
package/src/provider_runtime.py
CHANGED
|
@@ -3,6 +3,7 @@ import os
|
|
|
3
3
|
import signal
|
|
4
4
|
import shlex
|
|
5
5
|
import subprocess
|
|
6
|
+
import sys
|
|
6
7
|
from datetime import datetime, timezone
|
|
7
8
|
|
|
8
9
|
from .errors import CdxError
|
|
@@ -11,6 +12,21 @@ from .errors import CdxError
|
|
|
11
12
|
LOG_ROTATE_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
12
13
|
|
|
13
14
|
|
|
15
|
+
def _home_env_overrides(auth_home):
|
|
16
|
+
"""Return env vars that point the claude CLI to the given home directory.
|
|
17
|
+
|
|
18
|
+
On Unix, only HOME is needed. On Windows, Node.js resolves the home
|
|
19
|
+
directory via USERPROFILE (and falls back to HOMEDRIVE+HOMEPATH), so we
|
|
20
|
+
set all three to ensure profile isolation works regardless of the platform.
|
|
21
|
+
"""
|
|
22
|
+
overrides = {"HOME": auth_home}
|
|
23
|
+
if sys.platform == "win32":
|
|
24
|
+
overrides["USERPROFILE"] = auth_home
|
|
25
|
+
overrides["HOMEDRIVE"] = os.path.splitdrive(auth_home)[0] or "C:"
|
|
26
|
+
overrides["HOMEPATH"] = os.path.splitdrive(auth_home)[1] or auth_home
|
|
27
|
+
return overrides
|
|
28
|
+
|
|
29
|
+
|
|
14
30
|
def _get_auth_home(session):
|
|
15
31
|
return session.get("authHome") or session.get("sessionRoot") or session.get("codexHome", "")
|
|
16
32
|
|
|
@@ -91,7 +107,7 @@ def _build_launch_spec(session, cwd=None, env_override=None):
|
|
|
91
107
|
"args": ["--name", session["name"]],
|
|
92
108
|
"options": {
|
|
93
109
|
"cwd": cwd,
|
|
94
|
-
"env": {**env,
|
|
110
|
+
"env": {**env, **_home_env_overrides(_get_auth_home(session))},
|
|
95
111
|
},
|
|
96
112
|
"label": "claude",
|
|
97
113
|
}
|
|
@@ -108,7 +124,7 @@ def _build_launch_spec(session, cwd=None, env_override=None):
|
|
|
108
124
|
def _build_login_status_spec(session, env_override=None):
|
|
109
125
|
env = {**os.environ, **(env_override or {})}
|
|
110
126
|
if session["provider"] == "claude":
|
|
111
|
-
env
|
|
127
|
+
env.update(_home_env_overrides(_get_auth_home(session)))
|
|
112
128
|
|
|
113
129
|
def parser(output):
|
|
114
130
|
try:
|
|
@@ -133,7 +149,7 @@ def _build_auth_action_spec(session, action, cwd=None, env_override=None):
|
|
|
133
149
|
cwd = cwd or os.getcwd()
|
|
134
150
|
env = {**os.environ, **(env_override or {})}
|
|
135
151
|
if session["provider"] == "claude":
|
|
136
|
-
env
|
|
152
|
+
env.update(_home_env_overrides(_get_auth_home(session)))
|
|
137
153
|
return {"command": "claude", "args": ["auth", action],
|
|
138
154
|
"options": {"cwd": cwd, "env": env}, "label": f"claude auth {action}"}
|
|
139
155
|
env["CODEX_HOME"] = _get_auth_home(session)
|
|
@@ -165,7 +181,10 @@ def _probe_provider_auth(session, spawn_sync=None, env_override=None):
|
|
|
165
181
|
|
|
166
182
|
|
|
167
183
|
def _signal_exit_code(sig):
|
|
168
|
-
|
|
184
|
+
mapping = {signal.SIGINT: 130, signal.SIGTERM: 143}
|
|
185
|
+
if hasattr(signal, "SIGHUP"):
|
|
186
|
+
mapping[signal.SIGHUP] = 129
|
|
187
|
+
return mapping.get(sig, 1)
|
|
169
188
|
|
|
170
189
|
|
|
171
190
|
def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
@@ -210,7 +229,10 @@ def _run_interactive_provider_command(session, action, spawn=None, cwd=None,
|
|
|
210
229
|
handlers.append((sig, handler))
|
|
211
230
|
signal_emitter.on(sig, handler)
|
|
212
231
|
else:
|
|
213
|
-
|
|
232
|
+
_forward_sigs = [signal.SIGINT, signal.SIGTERM]
|
|
233
|
+
if hasattr(signal, "SIGHUP"):
|
|
234
|
+
_forward_sigs.append(signal.SIGHUP)
|
|
235
|
+
for sig in _forward_sigs:
|
|
214
236
|
try:
|
|
215
237
|
original_handlers[sig] = signal.signal(sig, forward)
|
|
216
238
|
except (OSError, ValueError):
|
package/src/session_store.py
CHANGED
|
@@ -54,23 +54,40 @@ def _fsync_directory(directory):
|
|
|
54
54
|
|
|
55
55
|
@contextmanager
|
|
56
56
|
def _file_lock(lock_path):
|
|
57
|
+
import sys
|
|
57
58
|
_ensure_dir(os.path.dirname(lock_path))
|
|
58
59
|
with open(lock_path, "a", encoding="utf-8") as lock:
|
|
59
|
-
|
|
60
|
-
import
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
if sys.platform == "win32":
|
|
61
|
+
import msvcrt
|
|
62
|
+
try:
|
|
63
|
+
lock.seek(0)
|
|
64
|
+
msvcrt.locking(lock.fileno(), msvcrt.LK_LOCK, 1)
|
|
65
|
+
except OSError as error:
|
|
66
|
+
raise CdxError(f"Failed to lock session store: {error}") from error
|
|
67
|
+
try:
|
|
68
|
+
yield
|
|
69
|
+
finally:
|
|
70
|
+
try:
|
|
71
|
+
lock.seek(0)
|
|
72
|
+
msvcrt.locking(lock.fileno(), msvcrt.LK_UNLCK, 1)
|
|
73
|
+
except OSError as error:
|
|
74
|
+
raise CdxError(f"Failed to unlock session store: {error}") from error
|
|
75
|
+
else:
|
|
70
76
|
try:
|
|
71
|
-
|
|
77
|
+
import fcntl
|
|
78
|
+
except ImportError as error:
|
|
79
|
+
raise CdxError("Session store locking requires fcntl on this platform") from error
|
|
80
|
+
try:
|
|
81
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
72
82
|
except OSError as error:
|
|
73
|
-
raise CdxError(f"Failed to
|
|
83
|
+
raise CdxError(f"Failed to lock session store: {error}") from error
|
|
84
|
+
try:
|
|
85
|
+
yield
|
|
86
|
+
finally:
|
|
87
|
+
try:
|
|
88
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_UN)
|
|
89
|
+
except OSError as error:
|
|
90
|
+
raise CdxError(f"Failed to unlock session store: {error}") from error
|
|
74
91
|
|
|
75
92
|
|
|
76
93
|
def create_session_store(base_dir):
|