@wangxt0223/codex-switcher 0.6.0 → 0.6.1
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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1 - 2026-04-12
|
|
4
|
+
|
|
5
|
+
- Fixed symlink invocation path resolution for `codex-sw` / `codex-switcher`, so global npm installs can always find bundled scripts.
|
|
6
|
+
- Fixed `list` row field reuse bug that could leak previous row values into later rows.
|
|
7
|
+
- Improved usage API request compatibility by adding `ChatGPT-Account-Id` and browser-like headers, while keeping local sessions fallback.
|
|
8
|
+
- Added smoke-test coverage for symlink launch path behavior.
|
|
9
|
+
|
|
3
10
|
## 0.6.0 - 2026-04-12
|
|
4
11
|
|
|
5
12
|
- Refactored core model from profile-based switching to `env + account` with built-in `default=~/.codex`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wangxt0223/codex-switcher",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Env + account switcher for Codex CLI and Codex App with shared env data and per-account auth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "wangxt",
|
|
@@ -43,5 +43,8 @@
|
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public",
|
|
45
45
|
"registry": "https://registry.npmjs.org/"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@wangxt0223/codex-switcher": "^0.6.0"
|
|
46
49
|
}
|
|
47
50
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
resolve_script_dir() {
|
|
5
|
+
local src="${BASH_SOURCE[0]}"
|
|
6
|
+
local dir
|
|
7
|
+
while [[ -h "$src" ]]; do
|
|
8
|
+
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
|
9
|
+
src="$(readlink "$src")"
|
|
10
|
+
[[ "$src" == /* ]] || src="$dir/$src"
|
|
11
|
+
done
|
|
12
|
+
cd -P "$(dirname "$src")" && pwd
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(resolve_script_dir)"
|
|
4
16
|
export CODEX_SWITCHER_INVOKED_AS="codex-sw"
|
|
5
17
|
exec "$SCRIPT_DIR/codex-switcher" "$@"
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
resolve_script_dir() {
|
|
5
|
+
local src="${BASH_SOURCE[0]}"
|
|
6
|
+
local dir
|
|
7
|
+
while [[ -h "$src" ]]; do
|
|
8
|
+
dir="$(cd -P "$(dirname "$src")" && pwd)"
|
|
9
|
+
src="$(readlink "$src")"
|
|
10
|
+
[[ "$src" == /* ]] || src="$dir/$src"
|
|
11
|
+
done
|
|
12
|
+
cd -P "$(dirname "$src")" && pwd
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(resolve_script_dir)"
|
|
16
|
+
|
|
4
17
|
STATE_DIR="${CODEX_SWITCHER_STATE_DIR:-$HOME/.codex-switcher}"
|
|
5
18
|
ENVS_DIR="${CODEX_SWITCHER_ENVS_DIR:-$HOME/.codex-envs}"
|
|
6
19
|
ACCOUNTS_DIR="${CODEX_SWITCHER_ACCOUNTS_DIR:-$STATE_DIR/env-accounts}"
|
|
@@ -85,9 +98,8 @@ now_utc() {
|
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
switcher_version() {
|
|
88
|
-
local
|
|
89
|
-
|
|
90
|
-
package_json="$(cd "$script_dir/../../.." && pwd)/package.json"
|
|
101
|
+
local package_json version
|
|
102
|
+
package_json="$(cd "$SCRIPT_DIR/../../.." && pwd)/package.json"
|
|
91
103
|
|
|
92
104
|
if [[ -f "$package_json" ]]; then
|
|
93
105
|
version="$(sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_json" | head -n 1)"
|
|
@@ -567,9 +579,7 @@ app_stop_managed() {
|
|
|
567
579
|
}
|
|
568
580
|
|
|
569
581
|
metrics_script_path() {
|
|
570
|
-
|
|
571
|
-
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
572
|
-
echo "$script_dir/profile-metrics.py"
|
|
582
|
+
echo "$SCRIPT_DIR/profile-metrics.py"
|
|
573
583
|
}
|
|
574
584
|
|
|
575
585
|
collect_profile_metrics_tsv() {
|
|
@@ -931,6 +941,12 @@ cmd_list() {
|
|
|
931
941
|
local found=0
|
|
932
942
|
while IFS= read -r account; do
|
|
933
943
|
found=1
|
|
944
|
+
email=""
|
|
945
|
+
plan=""
|
|
946
|
+
usage_5h=""
|
|
947
|
+
usage_weekly=""
|
|
948
|
+
last_activity=""
|
|
949
|
+
source=""
|
|
934
950
|
marks=""
|
|
935
951
|
[[ "$env" == "$cli_env" && "$account" == "$cli_account" ]] && marks="${marks} [cli-current]"
|
|
936
952
|
[[ "$env" == "$app_env" && "$account" == "$app_account" ]] && marks="${marks} [app-current]"
|
|
@@ -954,6 +970,12 @@ cmd_list() {
|
|
|
954
970
|
done < <(list_accounts_for_env "$env")
|
|
955
971
|
|
|
956
972
|
if [[ "$found" -eq 0 ]]; then
|
|
973
|
+
email=""
|
|
974
|
+
plan=""
|
|
975
|
+
usage_5h=""
|
|
976
|
+
usage_weekly=""
|
|
977
|
+
last_activity=""
|
|
978
|
+
source=""
|
|
957
979
|
metrics="$(collect_profile_metrics_tsv "-" "$home/auth.json" "$home" || true)"
|
|
958
980
|
if [[ -n "$metrics" ]]; then
|
|
959
981
|
IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
|
|
@@ -1242,7 +1264,7 @@ cmd_init() {
|
|
|
1242
1264
|
;;
|
|
1243
1265
|
esac
|
|
1244
1266
|
|
|
1245
|
-
script_abs="$
|
|
1267
|
+
script_abs="$SCRIPT_DIR/codex-switcher"
|
|
1246
1268
|
target_link="$HOME/.local/bin/codex-sw"
|
|
1247
1269
|
block_start="# >>> codex-sw init >>>"
|
|
1248
1270
|
block_end="# <<< codex-sw init <<<"
|
|
@@ -11,6 +11,10 @@ from typing import Any, Dict, Optional
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
USAGE_ENDPOINT = "https://chatgpt.com/backend-api/wham/usage"
|
|
14
|
+
DEFAULT_USER_AGENT = (
|
|
15
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
16
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
|
17
|
+
)
|
|
14
18
|
DASH = "-"
|
|
15
19
|
KNOWN_PLANS = {"free", "plus", "pro", "team", "business", "enterprise", "edu"}
|
|
16
20
|
|
|
@@ -165,7 +169,7 @@ def extract_windows_from_usage_blob(blob: Dict[str, Any]) -> Dict[int, Dict[str,
|
|
|
165
169
|
return windows
|
|
166
170
|
|
|
167
171
|
|
|
168
|
-
def request_usage(access_token: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
172
|
+
def request_usage(access_token: str, account_id: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
169
173
|
if not access_token:
|
|
170
174
|
return None
|
|
171
175
|
cmd = [
|
|
@@ -177,8 +181,14 @@ def request_usage(access_token: str, timeout_seconds: int) -> Optional[Dict[str,
|
|
|
177
181
|
str(max(2, timeout_seconds + 2)),
|
|
178
182
|
"-H",
|
|
179
183
|
f"Authorization: Bearer {access_token}",
|
|
180
|
-
|
|
184
|
+
"-H",
|
|
185
|
+
"Accept: application/json",
|
|
186
|
+
"-H",
|
|
187
|
+
f"User-Agent: {DEFAULT_USER_AGENT}",
|
|
181
188
|
]
|
|
189
|
+
if account_id:
|
|
190
|
+
cmd.extend(["-H", f"ChatGPT-Account-Id: {account_id}"])
|
|
191
|
+
cmd.append(USAGE_ENDPOINT)
|
|
182
192
|
try:
|
|
183
193
|
result = subprocess.run(
|
|
184
194
|
cmd,
|
|
@@ -276,8 +286,8 @@ def collect_local_metrics(data_path: str) -> Dict[str, Any]:
|
|
|
276
286
|
return {"source": "local"}
|
|
277
287
|
|
|
278
288
|
|
|
279
|
-
def collect_api_metrics(access_token: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
280
|
-
payload = request_usage(access_token, timeout_seconds)
|
|
289
|
+
def collect_api_metrics(access_token: str, account_id: str, timeout_seconds: int) -> Optional[Dict[str, Any]]:
|
|
290
|
+
payload = request_usage(access_token, account_id, timeout_seconds)
|
|
281
291
|
if not payload:
|
|
282
292
|
return None
|
|
283
293
|
|
|
@@ -318,6 +328,9 @@ def main() -> int:
|
|
|
318
328
|
access_token = tokens.get("access_token")
|
|
319
329
|
if not isinstance(access_token, str):
|
|
320
330
|
access_token = ""
|
|
331
|
+
account_id = tokens.get("account_id")
|
|
332
|
+
if not isinstance(account_id, str):
|
|
333
|
+
account_id = ""
|
|
321
334
|
id_token = tokens.get("id_token")
|
|
322
335
|
if not isinstance(id_token, str):
|
|
323
336
|
id_token = ""
|
|
@@ -331,7 +344,7 @@ def main() -> int:
|
|
|
331
344
|
if plan_from_claims == "unknown":
|
|
332
345
|
plan_from_claims = normalize_plan(auth_data.get("chatgpt_plan_type") or auth_data.get("plan_type"))
|
|
333
346
|
|
|
334
|
-
api_metrics = collect_api_metrics(access_token, args.timeout_seconds)
|
|
347
|
+
api_metrics = collect_api_metrics(access_token, account_id, args.timeout_seconds)
|
|
335
348
|
metrics = api_metrics if api_metrics is not None else collect_local_metrics(args.data_path)
|
|
336
349
|
|
|
337
350
|
windows = metrics.get("windows")
|
|
@@ -3,6 +3,7 @@ set -euo pipefail
|
|
|
3
3
|
|
|
4
4
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
5
5
|
SW="$ROOT/scripts/codex-sw"
|
|
6
|
+
SW_LINK=""
|
|
6
7
|
|
|
7
8
|
bash -n "$SW"
|
|
8
9
|
|
|
@@ -12,6 +13,8 @@ ENVS="$TMPBASE/envs"
|
|
|
12
13
|
DEFAULT_HOME="$TMPBASE/default-home"
|
|
13
14
|
BIN="$TMPBASE/bin"
|
|
14
15
|
mkdir -p "$BIN" "$DEFAULT_HOME"
|
|
16
|
+
SW_LINK="$BIN/codex-sw-link"
|
|
17
|
+
ln -s "$SW" "$SW_LINK"
|
|
15
18
|
|
|
16
19
|
cleanup() {
|
|
17
20
|
pkill -f "$BIN/fake-codex-app" >/dev/null 2>&1 || true
|
|
@@ -91,6 +94,9 @@ echo '{"memo":"persist"}' > "$DEFAULT_HOME/shared.json"
|
|
|
91
94
|
check_out="$("$SW" check)"
|
|
92
95
|
echo "$check_out" | grep -Eq '^version: [0-9]+\.[0-9]+\.[0-9]+$'
|
|
93
96
|
echo "$check_out" | grep -q "check: ok"
|
|
97
|
+
link_check_out="$("$SW_LINK" check)"
|
|
98
|
+
echo "$link_check_out" | grep -Eq '^version: [0-9]+\.[0-9]+\.[0-9]+$'
|
|
99
|
+
echo "$link_check_out" | grep -q "check: ok"
|
|
94
100
|
init_out="$("$SW" init --dry-run)"
|
|
95
101
|
echo "$init_out" | grep -q "\[dry-run\]"
|
|
96
102
|
"$SW" upgrade
|
|
@@ -171,6 +177,10 @@ grep -q "(api)" /tmp/codex_sw_list_api
|
|
|
171
177
|
grep -q "60% (" /tmp/codex_sw_list_api
|
|
172
178
|
grep -q "80% (" /tmp/codex_sw_list_api
|
|
173
179
|
|
|
180
|
+
"$SW_LINK" list >/tmp/codex_sw_list_symlink
|
|
181
|
+
grep -q "(personal)personal@example.com" /tmp/codex_sw_list_symlink
|
|
182
|
+
grep -q "(api)" /tmp/codex_sw_list_symlink
|
|
183
|
+
|
|
174
184
|
mkdir -p "$ENVS/project/home/sessions/2026/04/12"
|
|
175
185
|
cat > "$ENVS/project/home/sessions/2026/04/12/rollout-test.jsonl" <<'JSONL'
|
|
176
186
|
{"timestamp":"2026-04-12T09:00:00Z","type":"event_msg","payload":{"type":"token_count","rate_limits":{"plan_type":"business","primary":{"used_percent":25,"window_minutes":300,"resets_at":1776004200},"secondary":{"used_percent":70,"window_minutes":10080,"resets_at":1776519000}}}}
|