@wangxt0223/codex-switcher 0.5.0 → 0.6.0

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.
@@ -2,7 +2,12 @@
2
2
  set -euo pipefail
3
3
 
4
4
  STATE_DIR="${CODEX_SWITCHER_STATE_DIR:-$HOME/.codex-switcher}"
5
- PROFILES_DIR="${CODEX_SWITCHER_PROFILES_DIR:-$HOME/.codex-profiles}"
5
+ ENVS_DIR="${CODEX_SWITCHER_ENVS_DIR:-$HOME/.codex-envs}"
6
+ ACCOUNTS_DIR="${CODEX_SWITCHER_ACCOUNTS_DIR:-$STATE_DIR/env-accounts}"
7
+ DEFAULT_HOME="${CODEX_SWITCHER_DEFAULT_HOME:-$HOME/.codex}"
8
+ DEFAULT_ENV_NAME="default"
9
+ DEFAULT_ACCOUNT_NAME="default"
10
+
6
11
  APP_LOG="${CODEX_SWITCHER_APP_LOG:-$STATE_DIR/app.log}"
7
12
  SWITCH_LOG="${CODEX_SWITCHER_SWITCH_LOG:-$STATE_DIR/switcher.log}"
8
13
  APP_PID_FILE="$STATE_DIR/app.pid"
@@ -14,23 +19,37 @@ SCRIPT_NAME="${CODEX_SWITCHER_INVOKED_AS:-$(basename "$0")}"
14
19
  usage() {
15
20
  cat <<'USAGE'
16
21
  Usage:
17
- codex-sw add <profile>
18
- codex-sw remove <profile> [--force]
22
+ codex-sw env list
23
+ codex-sw env create <env> [--empty|--from-default|--from-env <src>]
24
+ codex-sw env use <env> [--target cli|app|both]
25
+ codex-sw env remove <env> [--force]
26
+ codex-sw env current [cli|app]
27
+ codex-sw env path [env]
28
+
29
+ codex-sw account list [--env <env>]
30
+ codex-sw account add <account> [--env <env>]
31
+ codex-sw account remove <account> [--env <env>] [--force]
32
+ codex-sw account login <account> [--env <env>] [--target cli|app|both] [--sync|--no-sync]
33
+ codex-sw account use <account> [--env <env>] [--target cli|app|both] [--sync|--no-sync]
34
+ codex-sw account logout [account] [--env <env>] [--target cli|app|both]
35
+ codex-sw account current [cli|app]
36
+
19
37
  codex-sw list
20
- codex-sw import-default <profile> [--with-auth] [--force]
21
- codex-sw use <profile> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]
22
- codex-sw switch <profile> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]
23
- codex-sw current [cli|app]
24
38
  codex-sw status
25
-
39
+ codex-sw current [cli|app]
26
40
  codex-sw exec -- <codex args...>
27
- codex-sw login [profile] [--sync|--no-sync]
28
- codex-sw logout [profile]
29
- codex-sw env [profile]
30
-
31
- codex-sw app open [profile] [-- <app args...>]
32
- codex-sw app use <profile> [-- <app args...>]
33
- codex-sw app logout [profile]
41
+ codex-sw login [account] [--sync|--no-sync]
42
+ codex-sw logout [account]
43
+
44
+ codex-sw use <account> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]
45
+ codex-sw switch <account> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]
46
+ codex-sw add <account>
47
+ codex-sw remove <account> [--force]
48
+ codex-sw import-default <env> [--with-auth] [--force]
49
+
50
+ codex-sw app open [account] [-- <app args...>]
51
+ codex-sw app use <account> [-- <app args...>]
52
+ codex-sw app logout [account]
34
53
  codex-sw app status
35
54
  codex-sw app stop
36
55
  codex-sw app current
@@ -44,22 +63,11 @@ Usage:
44
63
  Compatibility:
45
64
  codex-switcher <same-commands>
46
65
 
47
- Sync behavior:
48
- --sync enable overwrite sync (excluding auth.json)
49
- - login --sync: ~/.codex -> target profile
50
- - use/switch --sync: current CLI profile -> target profile
51
- --no-sync disable sync explicitly (default)
52
-
53
- Environment overrides:
54
- CODEX_SWITCHER_STATE_DIR
55
- CODEX_SWITCHER_PROFILES_DIR
56
- CODEX_SWITCHER_APP_BIN
57
- CODEX_SWITCHER_APP_LOG
58
- CODEX_SWITCHER_SWITCH_LOG
59
- CODEX_SWITCHER_LOCK_WAIT_SECONDS
60
- CODEX_SWITCHER_DEFAULT_HOME
61
- CODEX_SWITCHER_UPGRADE_REGISTRY
62
- CODEX_SWITCHER_NPM_PACKAGE
66
+ Notes:
67
+ - Built-in env "default" always maps to ~/.codex (or CODEX_SWITCHER_DEFAULT_HOME).
68
+ - Same-env account switch only swaps auth.json and does not sync shared data.
69
+ - `list` prints: EMAIL / PLAN / 5H USAGE / WEEKLY USAGE / LAST ACTIVITY (+ env/account context).
70
+ - Usage metrics are API-first and automatically fallback to local sessions; source is shown as (api|local).
63
71
  USAGE
64
72
  }
65
73
 
@@ -76,6 +84,18 @@ now_utc() {
76
84
  date -u +"%Y-%m-%dT%H:%M:%SZ"
77
85
  }
78
86
 
87
+ switcher_version() {
88
+ local script_dir package_json version
89
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
90
+ package_json="$(cd "$script_dir/../../.." && pwd)/package.json"
91
+
92
+ if [[ -f "$package_json" ]]; then
93
+ version="$(sed -nE 's/^[[:space:]]*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' "$package_json" | head -n 1)"
94
+ fi
95
+
96
+ echo "${version:-unknown}"
97
+ }
98
+
79
99
  redact() {
80
100
  sed -E \
81
101
  -e 's/sk-[A-Za-z0-9_-]{8,}/sk-***REDACTED***/g' \
@@ -92,8 +112,9 @@ log_event() {
92
112
  }
93
113
 
94
114
  ensure_dirs() {
95
- mkdir -p "$STATE_DIR" "$PROFILES_DIR"
96
- chmod 700 "$STATE_DIR" "$PROFILES_DIR" 2>/dev/null || true
115
+ mkdir -p "$STATE_DIR" "$ENVS_DIR" "$ACCOUNTS_DIR"
116
+ mkdir -p "$DEFAULT_HOME"
117
+ chmod 700 "$STATE_DIR" "$ENVS_DIR" "$ACCOUNTS_DIR" "$DEFAULT_HOME" 2>/dev/null || true
97
118
  }
98
119
 
99
120
  perm_of() {
@@ -105,77 +126,120 @@ perm_of() {
105
126
  fi
106
127
  }
107
128
 
108
- validate_profile_name_noexit() {
129
+ validate_name_noexit() {
109
130
  local name="${1:-}"
110
131
  [[ -n "$name" ]] || return 1
111
132
  [[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || return 1
112
133
  return 0
113
134
  }
114
135
 
115
- validate_profile_name() {
136
+ validate_env_name_noexit() {
116
137
  local name="${1:-}"
117
- validate_profile_name_noexit "$name" || err "invalid profile '$name' (allowed: A-Z a-z 0-9 . _ -)"
138
+ [[ "$name" == "$DEFAULT_ENV_NAME" ]] && return 0
139
+ validate_name_noexit "$name"
118
140
  }
119
141
 
120
- default_profile_for() {
121
- local target="$1"
122
- if [[ "$target" == "cli" ]]; then
123
- echo "default"
142
+ validate_env_name() {
143
+ local name="${1:-}"
144
+ validate_env_name_noexit "$name" || err "invalid env '$name' (allowed: A-Z a-z 0-9 . _ -, plus reserved: default)"
145
+ }
146
+
147
+ validate_account_name() {
148
+ local name="${1:-}"
149
+ validate_name_noexit "$name" || err "invalid account '$name' (allowed: A-Z a-z 0-9 . _ -)"
150
+ }
151
+
152
+ env_root_path() {
153
+ local env="$1"
154
+ echo "$ENVS_DIR/$env"
155
+ }
156
+
157
+ env_home_path() {
158
+ local env="$1"
159
+ if [[ "$env" == "$DEFAULT_ENV_NAME" ]]; then
160
+ echo "$DEFAULT_HOME"
124
161
  else
125
- echo "app-default"
162
+ echo "$ENVS_DIR/$env/home"
126
163
  fi
127
164
  }
128
165
 
129
- profile_path() {
130
- local name="$1"
131
- echo "$PROFILES_DIR/$name"
166
+ account_dir_path() {
167
+ local env="$1"
168
+ local account="$2"
169
+ echo "$ACCOUNTS_DIR/$env/$account"
132
170
  }
133
171
 
134
- ensure_profile() {
135
- local name="$1"
136
- local p
137
- p="$(profile_path "$name")"
138
- mkdir -p "$p"
139
- chmod 700 "$p" 2>/dev/null || true
172
+ account_auth_path() {
173
+ local env="$1"
174
+ local account="$2"
175
+ echo "$(account_dir_path "$env" "$account")/auth.json"
140
176
  }
141
177
 
142
- require_profile_exists() {
143
- local name="$1"
144
- local p
145
- p="$(profile_path "$name")"
146
- [[ -d "$p" ]] || err "profile '$name' not found. run: $SCRIPT_NAME add $name"
178
+ current_env_file() {
179
+ local target="$1"
180
+ echo "$STATE_DIR/current_${target}_env"
147
181
  }
148
182
 
149
- current_file() {
183
+ current_account_file() {
150
184
  local target="$1"
151
- echo "$STATE_DIR/current_${target}"
185
+ echo "$STATE_DIR/current_${target}_account"
152
186
  }
153
187
 
154
- set_current() {
188
+ set_current_env() {
155
189
  local target="$1"
156
- local profile="$2"
190
+ local env="$2"
157
191
  local file tmp
158
- file="$(current_file "$target")"
192
+ file="$(current_env_file "$target")"
159
193
  tmp="${file}.tmp"
160
- printf '%s\n' "$profile" > "$tmp"
194
+ printf '%s\n' "$env" > "$tmp"
161
195
  chmod 600 "$tmp" 2>/dev/null || true
162
196
  mv "$tmp" "$file"
163
197
  }
164
198
 
165
- read_current() {
199
+ set_current_account() {
166
200
  local target="$1"
167
- local file value default
168
- file="$(current_file "$target")"
169
- default="$(default_profile_for "$target")"
201
+ local account="$2"
202
+ local file tmp
203
+ file="$(current_account_file "$target")"
204
+ tmp="${file}.tmp"
205
+ printf '%s\n' "$account" > "$tmp"
206
+ chmod 600 "$tmp" 2>/dev/null || true
207
+ mv "$tmp" "$file"
208
+ }
209
+
210
+ read_current_env() {
211
+ local target="$1"
212
+ local file value
213
+ file="$(current_env_file "$target")"
214
+
215
+ if [[ ! -f "$file" ]]; then
216
+ echo "$DEFAULT_ENV_NAME"
217
+ return 1
218
+ fi
219
+
220
+ value="$(head -n 1 "$file" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
221
+ if ! validate_env_name_noexit "$value"; then
222
+ echo "$DEFAULT_ENV_NAME"
223
+ return 2
224
+ fi
225
+
226
+ echo "$value"
227
+ return 0
228
+ }
229
+
230
+ read_current_account() {
231
+ local target="$1"
232
+ local file value
233
+ file="$(current_account_file "$target")"
170
234
 
171
235
  if [[ ! -f "$file" ]]; then
172
- echo "$default"
236
+ echo "$DEFAULT_ACCOUNT_NAME"
173
237
  return 1
174
238
  fi
175
239
 
176
240
  value="$(head -n 1 "$file" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
177
- if ! validate_profile_name_noexit "$value"; then
178
- echo "$default"
241
+ if ! validate_name_noexit "$value"; then
242
+ echo "$DEFAULT_ACCOUNT_NAME"
179
243
  return 2
180
244
  fi
181
245
 
@@ -183,32 +247,150 @@ read_current() {
183
247
  return 0
184
248
  }
185
249
 
186
- first_profile_name() {
187
- local first
188
- first="$(find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | head -n 1)"
189
- if [[ -n "$first" ]]; then
190
- echo "$first"
250
+ ensure_env_home() {
251
+ local env="$1"
252
+ local home
253
+ validate_env_name "$env"
254
+ home="$(env_home_path "$env")"
255
+ mkdir -p "$home"
256
+ if [[ "$env" != "$DEFAULT_ENV_NAME" ]]; then
257
+ mkdir -p "$(env_root_path "$env")"
258
+ fi
259
+ chmod 700 "$home" 2>/dev/null || true
260
+ }
261
+
262
+ env_exists() {
263
+ local env="$1"
264
+ if [[ "$env" == "$DEFAULT_ENV_NAME" ]]; then
265
+ return 0
266
+ fi
267
+ [[ -d "$(env_home_path "$env")" ]]
268
+ }
269
+
270
+ require_env_exists() {
271
+ local env="$1"
272
+ validate_env_name "$env"
273
+ if [[ "$env" == "$DEFAULT_ENV_NAME" ]]; then
274
+ ensure_env_home "$env"
191
275
  return 0
192
276
  fi
277
+ [[ -d "$(env_home_path "$env")" ]] || err "env '$env' not found. run: $SCRIPT_NAME env create $env"
278
+ }
279
+
280
+ ensure_account_slot() {
281
+ local env="$1"
282
+ local account="$2"
283
+ validate_env_name "$env"
284
+ validate_account_name "$account"
285
+ mkdir -p "$(account_dir_path "$env" "$account")"
286
+ chmod 700 "$(account_dir_path "$env" "$account")" 2>/dev/null || true
287
+ }
288
+
289
+ account_has_auth() {
290
+ local env="$1"
291
+ local account="$2"
292
+ local auth_file home_auth
293
+ auth_file="$(account_auth_path "$env" "$account")"
294
+ if [[ -f "$auth_file" ]]; then
295
+ return 0
296
+ fi
297
+ if [[ "$account" == "$DEFAULT_ACCOUNT_NAME" ]]; then
298
+ home_auth="$(env_home_path "$env")/auth.json"
299
+ [[ -f "$home_auth" ]] && return 0
300
+ fi
193
301
  return 1
194
302
  }
195
303
 
196
- resolve_app_bin() {
197
- if [[ -n "${CODEX_SWITCHER_APP_BIN:-}" ]]; then
198
- [[ -x "$CODEX_SWITCHER_APP_BIN" ]] || err "CODEX_SWITCHER_APP_BIN is not executable: $CODEX_SWITCHER_APP_BIN"
199
- echo "$CODEX_SWITCHER_APP_BIN"
200
- return
304
+ list_env_names() {
305
+ echo "$DEFAULT_ENV_NAME"
306
+ if [[ -d "$ENVS_DIR" ]]; then
307
+ find "$ENVS_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | while IFS= read -r env; do
308
+ validate_env_name_noexit "$env" || continue
309
+ [[ "$env" == "$DEFAULT_ENV_NAME" ]] && continue
310
+ [[ -d "$(env_home_path "$env")" ]] || continue
311
+ echo "$env"
312
+ done
201
313
  fi
314
+ }
202
315
 
203
- if [[ -x "/Applications/Codex.app/Contents/MacOS/Codex" ]]; then
204
- echo "/Applications/Codex.app/Contents/MacOS/Codex"
205
- return
316
+ list_accounts_for_env() {
317
+ local env="$1"
318
+ local dir auth_file any
319
+ dir="$ACCOUNTS_DIR/$env"
320
+ any=0
321
+
322
+ if [[ -d "$dir" ]]; then
323
+ while IFS= read -r account; do
324
+ validate_name_noexit "$account" || continue
325
+ auth_file="$(account_auth_path "$env" "$account")"
326
+ if [[ -f "$auth_file" ]]; then
327
+ any=1
328
+ echo "$account"
329
+ fi
330
+ done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
206
331
  fi
207
- if [[ -x "$HOME/Applications/Codex.app/Contents/MacOS/Codex" ]]; then
208
- echo "$HOME/Applications/Codex.app/Contents/MacOS/Codex"
209
- return
332
+
333
+ if [[ "$any" -eq 0 ]] && [[ "$env" == "$DEFAULT_ENV_NAME" ]] && [[ -f "$(env_home_path "$env")/auth.json" ]]; then
334
+ echo "$DEFAULT_ACCOUNT_NAME"
210
335
  fi
211
- err "Codex.app binary not found. set CODEX_SWITCHER_APP_BIN manually"
336
+ }
337
+
338
+ copy_auth_file() {
339
+ local src="$1"
340
+ local dst="$2"
341
+ local tmp
342
+ tmp="${dst}.tmp"
343
+ cp "$src" "$tmp"
344
+ chmod 600 "$tmp" 2>/dev/null || true
345
+ mv "$tmp" "$dst"
346
+ }
347
+
348
+ save_env_auth_to_account() {
349
+ local env="$1"
350
+ local account="$2"
351
+ local home_auth dst
352
+ home_auth="$(env_home_path "$env")/auth.json"
353
+ [[ -f "$home_auth" ]] || return 1
354
+ ensure_account_slot "$env" "$account"
355
+ dst="$(account_auth_path "$env" "$account")"
356
+ copy_auth_file "$home_auth" "$dst"
357
+ }
358
+
359
+ clear_env_auth() {
360
+ local env="$1"
361
+ rm -f -- "$(env_home_path "$env")/auth.json"
362
+ }
363
+
364
+ install_account_auth_to_env() {
365
+ local env="$1"
366
+ local account="$2"
367
+ local src dst
368
+ src="$(account_auth_path "$env" "$account")"
369
+ dst="$(env_home_path "$env")/auth.json"
370
+ if [[ -f "$src" ]]; then
371
+ copy_auth_file "$src" "$dst"
372
+ return 0
373
+ fi
374
+ if [[ "$account" == "$DEFAULT_ACCOUNT_NAME" ]] && [[ -f "$dst" ]]; then
375
+ return 0
376
+ fi
377
+ rm -f -- "$dst"
378
+ }
379
+
380
+ sync_overwrite_excluding_auth() {
381
+ local src="$1"
382
+ local dst="$2"
383
+
384
+ [[ -d "$src" ]] || err "sync source not found: $src"
385
+ mkdir -p "$dst"
386
+
387
+ if command -v rsync >/dev/null 2>&1; then
388
+ rsync -a --checksum --delete --exclude 'auth.json' "$src/" "$dst/"
389
+ else
390
+ find "$dst" -mindepth 1 -maxdepth 1 ! -name 'auth.json' -exec rm -rf -- {} + 2>/dev/null || true
391
+ (cd "$src" && tar cf - --exclude ./auth.json .) | (cd "$dst" && tar xpf -)
392
+ fi
393
+ chmod 700 "$dst" 2>/dev/null || true
212
394
  }
213
395
 
214
396
  acquire_lock() {
@@ -259,24 +441,60 @@ with_lock() {
259
441
  trap - EXIT INT TERM
260
442
  }
261
443
 
262
- run_codex_for_profile() {
263
- local profile="$1"
444
+ with_targets() {
445
+ local target="$1"
446
+ shift
447
+ local fn="$1"
264
448
  shift
265
- require_profile_exists "$profile"
266
- CODEX_HOME="$(profile_path "$profile")" command codex "$@"
449
+ case "$target" in
450
+ cli|app)
451
+ "$fn" "$target" "$@"
452
+ ;;
453
+ both)
454
+ "$fn" cli "$@"
455
+ "$fn" app "$@"
456
+ ;;
457
+ *)
458
+ err "invalid target '$target' (use cli|app|both)"
459
+ ;;
460
+ esac
267
461
  }
268
462
 
269
- login_state_for_profile() {
270
- local profile="$1"
271
- local p rc
272
- p="$(profile_path "$profile")"
273
- if [[ ! -d "$p" ]]; then
274
- echo "missing-profile"
275
- return
463
+ target_home_path() {
464
+ local target="$1"
465
+ local env
466
+ env="$(read_current_env "$target" || true)"
467
+ env_home_path "$env"
468
+ }
469
+
470
+ ensure_target_auth_installed() {
471
+ local target="$1"
472
+ local env account
473
+ env="$(read_current_env "$target" || true)"
474
+ account="$(read_current_account "$target" || true)"
475
+ ensure_env_home "$env"
476
+ if account_has_auth "$env" "$account"; then
477
+ install_account_auth_to_env "$env" "$account"
478
+ else
479
+ clear_env_auth "$env"
276
480
  fi
481
+ }
482
+
483
+ run_codex_for_target() {
484
+ local target="$1"
485
+ shift
486
+ ensure_target_auth_installed "$target"
487
+ CODEX_HOME="$(target_home_path "$target")" command codex "$@"
488
+ }
489
+
490
+ login_state_for_target() {
491
+ local target="$1"
492
+ local rc home
493
+ ensure_target_auth_installed "$target"
494
+ home="$(target_home_path "$target")"
277
495
 
278
496
  rc=0
279
- CODEX_HOME="$p" command codex login status >/dev/null 2>&1 || rc=$?
497
+ CODEX_HOME="$home" command codex login status >/dev/null 2>&1 || rc=$?
280
498
  if [[ "$rc" -eq 0 ]]; then
281
499
  echo "logged-in"
282
500
  else
@@ -284,6 +502,24 @@ login_state_for_profile() {
284
502
  fi
285
503
  }
286
504
 
505
+ resolve_app_bin() {
506
+ if [[ -n "${CODEX_SWITCHER_APP_BIN:-}" ]]; then
507
+ [[ -x "$CODEX_SWITCHER_APP_BIN" ]] || err "CODEX_SWITCHER_APP_BIN is not executable: $CODEX_SWITCHER_APP_BIN"
508
+ echo "$CODEX_SWITCHER_APP_BIN"
509
+ return
510
+ fi
511
+
512
+ if [[ -x "/Applications/Codex.app/Contents/MacOS/Codex" ]]; then
513
+ echo "/Applications/Codex.app/Contents/MacOS/Codex"
514
+ return
515
+ fi
516
+ if [[ -x "$HOME/Applications/Codex.app/Contents/MacOS/Codex" ]]; then
517
+ echo "$HOME/Applications/Codex.app/Contents/MacOS/Codex"
518
+ return
519
+ fi
520
+ err "Codex.app binary not found. set CODEX_SWITCHER_APP_BIN manually"
521
+ }
522
+
287
523
  kill_tree() {
288
524
  local pid="$1"
289
525
  local child
@@ -330,194 +566,461 @@ app_stop_managed() {
330
566
  echo "Stopped managed app process"
331
567
  }
332
568
 
333
- prompt_confirm_remove() {
334
- local profile="$1"
335
- if [[ ! -t 0 ]]; then
336
- err "non-interactive terminal; rerun with --force"
569
+ metrics_script_path() {
570
+ local script_dir
571
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
572
+ echo "$script_dir/profile-metrics.py"
573
+ }
574
+
575
+ collect_profile_metrics_tsv() {
576
+ local account="$1"
577
+ local auth_file="$2"
578
+ local data_path="$3"
579
+ local script_path
580
+ script_path="$(metrics_script_path)"
581
+ [[ -f "$script_path" ]] || return 1
582
+ python3 "$script_path" --account-name "$account" --auth-file "$auth_file" --data-path "$data_path" 2>/dev/null
583
+ }
584
+
585
+ cmd_env_list() {
586
+ local cli_env app_env env marks
587
+ cli_env="$(read_current_env cli || true)"
588
+ app_env="$(read_current_env app || true)"
589
+ list_env_names | sort -u | while IFS= read -r env; do
590
+ marks=""
591
+ [[ "$env" == "$cli_env" ]] && marks="${marks} [cli-current]"
592
+ [[ "$env" == "$app_env" ]] && marks="${marks} [app-current]"
593
+ echo "- $env$marks"
594
+ done
595
+ }
596
+
597
+ cmd_env_create() {
598
+ local env="$1"
599
+ local mode="$2"
600
+ local src_env="${3:-}"
601
+ local home src
602
+
603
+ validate_env_name "$env"
604
+ [[ "$env" != "$DEFAULT_ENV_NAME" ]] || err "cannot create reserved env '$DEFAULT_ENV_NAME'"
605
+ if env_exists "$env"; then
606
+ err "env '$env' already exists"
337
607
  fi
338
- local answer
339
- printf "Remove profile '%s'? This deletes %s [y/N]: " "$profile" "$(profile_path "$profile")" >&2
340
- read -r answer
341
- case "$answer" in
342
- y|Y|yes|YES)
343
- return 0
608
+
609
+ home="$(env_home_path "$env")"
610
+ mkdir -p "$home"
611
+ chmod 700 "$home" 2>/dev/null || true
612
+
613
+ case "$mode" in
614
+ empty)
615
+ :
616
+ ;;
617
+ from-default)
618
+ sync_overwrite_excluding_auth "$DEFAULT_HOME" "$home"
619
+ ;;
620
+ from-env)
621
+ validate_env_name "$src_env"
622
+ require_env_exists "$src_env"
623
+ src="$(env_home_path "$src_env")"
624
+ sync_overwrite_excluding_auth "$src" "$home"
344
625
  ;;
345
626
  *)
346
- echo "Cancelled"
347
- return 1
627
+ err "unknown env create mode: $mode"
348
628
  ;;
349
629
  esac
630
+
631
+ log_event INFO "env_create env=$env mode=$mode src=$src_env"
632
+ echo "Created env: $env"
350
633
  }
351
634
 
352
- cmd_add() {
353
- local profile="$1"
354
- validate_profile_name "$profile"
355
- ensure_profile "$profile"
356
- log_event INFO "profile_add profile=$profile"
357
- echo "Added profile: $profile"
635
+ _cmd_env_use_target() {
636
+ local target="$1"
637
+ local env="$2"
638
+ local account
639
+
640
+ account="$(read_current_account "$target" || true)"
641
+ set_current_env "$target" "$env"
642
+ if account_has_auth "$env" "$account"; then
643
+ set_current_account "$target" "$account"
644
+ install_account_auth_to_env "$env" "$account"
645
+ else
646
+ set_current_account "$target" "$DEFAULT_ACCOUNT_NAME"
647
+ if account_has_auth "$env" "$DEFAULT_ACCOUNT_NAME"; then
648
+ install_account_auth_to_env "$env" "$DEFAULT_ACCOUNT_NAME"
649
+ else
650
+ clear_env_auth "$env"
651
+ fi
652
+ fi
358
653
  }
359
654
 
360
- cmd_remove() {
361
- local profile="$1"
362
- local force="${2:-false}"
363
- local cli_cur app_cur
655
+ cmd_env_use() {
656
+ local env="$1"
657
+ local target="$2"
364
658
 
365
- validate_profile_name "$profile"
366
- require_profile_exists "$profile"
659
+ require_env_exists "$env"
660
+ with_targets "$target" _cmd_env_use_target "$env"
661
+ log_event INFO "env_use env=$env target=$target"
662
+ echo "Switched $target env to: $env"
663
+ }
367
664
 
368
- cli_cur="$(read_current cli || true)"
369
- app_cur="$(read_current app || true)"
665
+ cmd_env_remove() {
666
+ local env="$1"
667
+ local force="$2"
668
+ local cli_env app_env
370
669
 
371
- if [[ "$profile" == "$cli_cur" && "$force" != "true" ]]; then
372
- err "profile '$profile' is current CLI profile; use --force to remove"
373
- fi
374
- if [[ "$profile" == "$app_cur" && "$force" != "true" ]]; then
375
- err "profile '$profile' is current App profile; use --force to remove"
376
- fi
670
+ validate_env_name "$env"
671
+ [[ "$env" != "$DEFAULT_ENV_NAME" ]] || err "cannot remove reserved env '$DEFAULT_ENV_NAME'"
672
+ env_exists "$env" || err "env '$env' not found"
673
+
674
+ cli_env="$(read_current_env cli || true)"
675
+ app_env="$(read_current_env app || true)"
377
676
 
378
- if [[ "$force" != "true" ]]; then
379
- prompt_confirm_remove "$profile" || return 0
677
+ if [[ "$env" == "$cli_env" && "$force" != "true" ]]; then
678
+ err "env '$env' is current CLI env; use --force to remove"
679
+ fi
680
+ if [[ "$env" == "$app_env" && "$force" != "true" ]]; then
681
+ err "env '$env' is current App env; use --force to remove"
380
682
  fi
381
683
 
382
- if [[ "$profile" == "$app_cur" ]]; then
684
+ if [[ "$env" == "$app_env" ]]; then
383
685
  app_stop_managed >/dev/null
384
- set_current app "$(default_profile_for app)"
686
+ set_current_env app "$DEFAULT_ENV_NAME"
687
+ set_current_account app "$DEFAULT_ACCOUNT_NAME"
688
+ ensure_target_auth_installed app
385
689
  fi
386
- if [[ "$profile" == "$cli_cur" ]]; then
387
- set_current cli "$(default_profile_for cli)"
690
+ if [[ "$env" == "$cli_env" ]]; then
691
+ set_current_env cli "$DEFAULT_ENV_NAME"
692
+ set_current_account cli "$DEFAULT_ACCOUNT_NAME"
693
+ ensure_target_auth_installed cli
388
694
  fi
389
695
 
390
- rm -rf -- "$(profile_path "$profile")"
391
- log_event INFO "profile_remove profile=$profile force=$force"
392
- echo "Removed profile: $profile"
696
+ rm -rf -- "$(env_root_path "$env")" "$ACCOUNTS_DIR/$env"
697
+ log_event INFO "env_remove env=$env force=$force"
698
+ echo "Removed env: $env"
393
699
  }
394
700
 
395
- cmd_list() {
396
- local cli_cur app_cur
397
- cli_cur="$(read_current cli || true)"
398
- app_cur="$(read_current app || true)"
701
+ cmd_env_current() {
702
+ local target="${1:-all}"
703
+ case "$target" in
704
+ cli)
705
+ read_current_env cli || true
706
+ ;;
707
+ app)
708
+ read_current_env app || true
709
+ ;;
710
+ all)
711
+ echo "cli: $(read_current_env cli || true)"
712
+ echo "app: $(read_current_env app || true)"
713
+ ;;
714
+ *)
715
+ err "invalid target '$target' (use cli|app)"
716
+ ;;
717
+ esac
718
+ }
399
719
 
400
- if ! find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d | grep -q .; then
401
- echo "No profiles yet. Use: $SCRIPT_NAME add <profile>"
402
- return 0
403
- fi
720
+ cmd_env_path() {
721
+ local env="${1:-$(read_current_env cli || true)}"
722
+ validate_env_name "$env"
723
+ require_env_exists "$env"
724
+ echo "export CODEX_HOME='$(env_home_path "$env")'"
725
+ }
404
726
 
405
- find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | while IFS= read -r name; do
406
- local marks
407
- marks=""
408
- [[ "$name" == "$cli_cur" ]] && marks="${marks} [cli-current]"
409
- [[ "$name" == "$app_cur" ]] && marks="${marks} [app-current]"
410
- echo "- $name$marks"
411
- done
727
+ cmd_account_add() {
728
+ local account="$1"
729
+ local env="$2"
730
+ validate_account_name "$account"
731
+ require_env_exists "$env"
732
+ ensure_account_slot "$env" "$account"
733
+ log_event INFO "account_add env=$env account=$account"
734
+ echo "Added account slot: $env/$account"
412
735
  }
413
736
 
414
- cmd_import_default() {
415
- local profile="$1"
416
- local with_auth="$2"
737
+ cmd_account_remove() {
738
+ local account="$1"
739
+ local env="$2"
417
740
  local force="$3"
418
- local src dst
741
+ local cli_env app_env cli_account app_account
419
742
 
420
- src="${CODEX_SWITCHER_DEFAULT_HOME:-$HOME/.codex}"
421
- validate_profile_name "$profile"
422
- [[ -d "$src" ]] || err "default codex home not found: $src"
423
- ensure_profile "$profile"
424
- dst="$(profile_path "$profile")"
743
+ validate_account_name "$account"
744
+ validate_env_name "$env"
425
745
 
426
- if [[ "$force" == "true" ]]; then
427
- find "$dst" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
428
- else
429
- if find "$dst" -mindepth 1 -maxdepth 1 | grep -q .; then
430
- err "profile '$profile' is not empty. use --force to overwrite"
431
- fi
746
+ if [[ ! -d "$(account_dir_path "$env" "$account")" ]] && ! account_has_auth "$env" "$account"; then
747
+ err "account '$account' not found in env '$env'"
432
748
  fi
433
749
 
434
- if command -v rsync >/dev/null 2>&1; then
435
- if [[ "$with_auth" == "true" ]]; then
436
- rsync -a "$src/" "$dst/"
437
- else
438
- rsync -a --exclude 'auth.json' "$src/" "$dst/"
439
- fi
440
- else
441
- if [[ "$with_auth" == "true" ]]; then
442
- (cd "$src" && tar cf - .) | (cd "$dst" && tar xpf -)
443
- else
444
- (cd "$src" && tar cf - --exclude ./auth.json .) | (cd "$dst" && tar xpf -)
445
- fi
750
+ cli_env="$(read_current_env cli || true)"
751
+ app_env="$(read_current_env app || true)"
752
+ cli_account="$(read_current_account cli || true)"
753
+ app_account="$(read_current_account app || true)"
754
+
755
+ if [[ "$env" == "$cli_env" && "$account" == "$cli_account" && "$force" != "true" ]]; then
756
+ err "account '$account' is current CLI account in env '$env'; use --force to remove"
757
+ fi
758
+ if [[ "$env" == "$app_env" && "$account" == "$app_account" && "$force" != "true" ]]; then
759
+ err "account '$account' is current App account in env '$env'; use --force to remove"
446
760
  fi
447
761
 
448
- chmod 700 "$dst" 2>/dev/null || true
449
- log_event INFO "import_default profile=$profile src=$src with_auth=$with_auth force=$force"
450
- echo "Imported default data to profile: $profile"
451
- if [[ "$with_auth" != "true" ]]; then
452
- echo "Auth not imported. Run: $SCRIPT_NAME login $profile"
762
+ rm -rf -- "$(account_dir_path "$env" "$account")"
763
+ if [[ "$account" == "$DEFAULT_ACCOUNT_NAME" ]]; then
764
+ rm -f -- "$(env_home_path "$env")/auth.json"
765
+ fi
766
+
767
+ if [[ "$env" == "$cli_env" && "$account" == "$cli_account" ]]; then
768
+ set_current_account cli "$DEFAULT_ACCOUNT_NAME"
769
+ ensure_target_auth_installed cli
770
+ fi
771
+ if [[ "$env" == "$app_env" && "$account" == "$app_account" ]]; then
772
+ set_current_account app "$DEFAULT_ACCOUNT_NAME"
773
+ ensure_target_auth_installed app
453
774
  fi
775
+
776
+ log_event INFO "account_remove env=$env account=$account force=$force"
777
+ echo "Removed account slot: $env/$account"
454
778
  }
455
779
 
456
- sync_overwrite_excluding_auth() {
457
- local src="$1"
458
- local dst="$2"
780
+ cmd_account_list() {
781
+ local env="$1"
782
+ local cli_env app_env cli_account app_account found account marks
783
+ require_env_exists "$env"
459
784
 
460
- [[ -d "$src" ]] || err "sync source not found: $src"
461
- mkdir -p "$dst"
785
+ cli_env="$(read_current_env cli || true)"
786
+ app_env="$(read_current_env app || true)"
787
+ cli_account="$(read_current_account cli || true)"
788
+ app_account="$(read_current_account app || true)"
789
+ found=0
462
790
 
463
- if command -v rsync >/dev/null 2>&1; then
464
- rsync -a --checksum --delete --exclude 'auth.json' "$src/" "$dst/"
465
- else
466
- find "$dst" -mindepth 1 -maxdepth 1 ! -name 'auth.json' -exec rm -rf -- {} + 2>/dev/null || true
467
- (cd "$src" && tar cf - --exclude ./auth.json .) | (cd "$dst" && tar xpf -)
791
+ while IFS= read -r account; do
792
+ found=1
793
+ marks=""
794
+ [[ "$env" == "$cli_env" && "$account" == "$cli_account" ]] && marks="${marks} [cli-current]"
795
+ [[ "$env" == "$app_env" && "$account" == "$app_account" ]] && marks="${marks} [app-current]"
796
+ echo "- $account$marks"
797
+ done < <(list_accounts_for_env "$env")
798
+
799
+ if [[ "$found" -eq 0 ]]; then
800
+ echo "No accounts in env '$env'. Use: $SCRIPT_NAME account login <account> --env $env"
468
801
  fi
469
- chmod 700 "$dst" 2>/dev/null || true
470
802
  }
471
803
 
472
- sync_default_to_profile() {
473
- local profile="$1"
474
- local src dst
475
- src="${CODEX_SWITCHER_DEFAULT_HOME:-$HOME/.codex}"
476
- dst="$(profile_path "$profile")"
477
- [[ -d "$src" ]] || err "default codex home not found: $src"
478
- ensure_profile "$profile"
479
- sync_overwrite_excluding_auth "$src" "$dst"
480
- log_event INFO "sync_default profile=$profile src=$src"
481
- echo "Synced default data to profile: $profile (auth.json excluded)"
482
- }
483
-
484
- sync_profile_to_profile() {
485
- local from_profile="$1"
486
- local to_profile="$2"
487
- local src dst
804
+ _cmd_account_set_target() {
805
+ local target="$1"
806
+ local env="$2"
807
+ local account="$3"
808
+ set_current_env "$target" "$env"
809
+ set_current_account "$target" "$account"
810
+ install_account_auth_to_env "$env" "$account"
811
+ }
488
812
 
489
- if [[ "$from_profile" == "$to_profile" ]]; then
490
- return 0
813
+ cmd_account_login() {
814
+ local account="$1"
815
+ local env="$2"
816
+ local target="$3"
817
+ local sync_mode="$4"
818
+ local home
819
+
820
+ validate_account_name "$account"
821
+ require_env_exists "$env"
822
+ ensure_env_home "$env"
823
+ home="$(env_home_path "$env")"
824
+
825
+ if [[ "$sync_mode" == "true" && "$env" != "$DEFAULT_ENV_NAME" ]]; then
826
+ sync_overwrite_excluding_auth "$DEFAULT_HOME" "$home"
491
827
  fi
492
828
 
493
- src="$(profile_path "$from_profile")"
494
- dst="$(profile_path "$to_profile")"
495
- require_profile_exists "$from_profile"
496
- ensure_profile "$to_profile"
497
- sync_overwrite_excluding_auth "$src" "$dst"
498
- log_event INFO "sync_profile from=$from_profile to=$to_profile"
499
- echo "Synced profile data: $from_profile -> $to_profile (auth.json excluded)"
829
+ CODEX_HOME="$home" command codex login
830
+ save_env_auth_to_account "$env" "$account" || err "login did not produce auth.json in $home"
831
+
832
+ with_targets "$target" _cmd_account_set_target "$env" "$account"
833
+
834
+ log_event INFO "account_login env=$env account=$account target=$target sync=$sync_mode"
835
+ echo "Logged in account: $env/$account"
500
836
  }
501
837
 
502
- cmd_use() {
503
- local profile="$1"
504
- local sync_mode="${2:-false}"
505
- local from_profile
506
- validate_profile_name "$profile"
838
+ cmd_account_use() {
839
+ local account="$1"
840
+ local env="$2"
841
+ local target="$3"
842
+ local sync_mode="$4"
843
+
844
+ validate_account_name "$account"
845
+ require_env_exists "$env"
846
+ account_has_auth "$env" "$account" || err "account '$account' in env '$env' has no auth.json. run: $SCRIPT_NAME account login $account --env $env"
507
847
 
508
848
  if [[ "$sync_mode" == "true" ]]; then
509
- from_profile="$(read_current cli || true)"
510
- validate_profile_name "$from_profile"
511
- sync_profile_to_profile "$from_profile" "$profile"
849
+ warn "same-env account switch only replaces auth.json; --sync is ignored"
512
850
  fi
513
851
 
514
- ensure_profile "$profile"
515
- set_current cli "$profile"
516
- log_event INFO "cli_use profile=$profile sync=$sync_mode"
852
+ with_targets "$target" _cmd_account_set_target "$env" "$account"
853
+ log_event INFO "account_use env=$env account=$account target=$target"
854
+ echo "Switched $target account to: $env/$account"
517
855
  }
518
856
 
519
- cmd_switch() {
520
- cmd_use "$1" "${2:-false}"
857
+ cmd_account_logout() {
858
+ local account="$1"
859
+ local env="$2"
860
+ local target="$3"
861
+ local cli_env app_env cli_account app_account
862
+
863
+ validate_env_name "$env"
864
+ if [[ -z "$account" ]]; then
865
+ if [[ "$target" == "both" ]]; then
866
+ account="$(read_current_account cli || true)"
867
+ else
868
+ account="$(read_current_account "$target" || true)"
869
+ fi
870
+ fi
871
+ validate_account_name "$account"
872
+ require_env_exists "$env"
873
+
874
+ rm -f -- "$(account_auth_path "$env" "$account")"
875
+
876
+ cli_env="$(read_current_env cli || true)"
877
+ app_env="$(read_current_env app || true)"
878
+ cli_account="$(read_current_account cli || true)"
879
+ app_account="$(read_current_account app || true)"
880
+
881
+ if [[ "$env" == "$cli_env" && "$account" == "$cli_account" ]]; then
882
+ set_current_account cli "$DEFAULT_ACCOUNT_NAME"
883
+ ensure_target_auth_installed cli
884
+ fi
885
+ if [[ "$env" == "$app_env" && "$account" == "$app_account" ]]; then
886
+ set_current_account app "$DEFAULT_ACCOUNT_NAME"
887
+ ensure_target_auth_installed app
888
+ fi
889
+
890
+ if [[ "$account" == "$DEFAULT_ACCOUNT_NAME" ]]; then
891
+ rm -f -- "$(env_home_path "$env")/auth.json"
892
+ fi
893
+
894
+ log_event INFO "account_logout env=$env account=$account target=$target"
895
+ echo "Logged out account: $env/$account"
896
+ }
897
+
898
+ cmd_account_current() {
899
+ local target="${1:-all}"
900
+ case "$target" in
901
+ cli)
902
+ echo "$(read_current_env cli || true)/$(read_current_account cli || true)"
903
+ ;;
904
+ app)
905
+ echo "$(read_current_env app || true)/$(read_current_account app || true)"
906
+ ;;
907
+ all)
908
+ echo "cli: $(read_current_env cli || true)/$(read_current_account cli || true)"
909
+ echo "app: $(read_current_env app || true)/$(read_current_account app || true)"
910
+ ;;
911
+ *)
912
+ err "invalid target '$target' (use cli|app)"
913
+ ;;
914
+ esac
915
+ }
916
+
917
+ cmd_list() {
918
+ local cli_env app_env cli_account app_account
919
+ local env account marks auth_file home metrics
920
+ local email plan usage_5h usage_weekly last_activity source
921
+
922
+ cli_env="$(read_current_env cli || true)"
923
+ app_env="$(read_current_env app || true)"
924
+ cli_account="$(read_current_account cli || true)"
925
+ app_account="$(read_current_account app || true)"
926
+
927
+ printf '%-12s %-30s %-44s %-10s %-16s %-18s %s\n' "ENV" "ACCOUNT" "EMAIL" "PLAN" "5H USAGE" "WEEKLY USAGE" "LAST ACTIVITY"
928
+ while IFS= read -r env; do
929
+ home="$(env_home_path "$env")"
930
+ ensure_env_home "$env"
931
+ local found=0
932
+ while IFS= read -r account; do
933
+ found=1
934
+ marks=""
935
+ [[ "$env" == "$cli_env" && "$account" == "$cli_account" ]] && marks="${marks} [cli-current]"
936
+ [[ "$env" == "$app_env" && "$account" == "$app_account" ]] && marks="${marks} [app-current]"
937
+ auth_file="$(account_auth_path "$env" "$account")"
938
+ [[ -f "$auth_file" ]] || auth_file="$home/auth.json"
939
+
940
+ metrics="$(collect_profile_metrics_tsv "$account" "$auth_file" "$home" || true)"
941
+ if [[ -n "$metrics" ]]; then
942
+ IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
943
+ fi
944
+ email="${email:-($account)-}"
945
+ plan="${plan:-unknown}"
946
+ usage_5h="${usage_5h:--}"
947
+ usage_weekly="${usage_weekly:--}"
948
+ last_activity="${last_activity:--}"
949
+ source="${source:-local}"
950
+ [[ "$source" == "api" || "$source" == "local" ]] || source="local"
951
+ last_activity="${last_activity} (${source})"
952
+
953
+ printf '%-12s %-30s %-44s %-10s %-16s %-18s %s\n' "$env" "$account$marks" "$email" "$plan" "$usage_5h" "$usage_weekly" "$last_activity"
954
+ done < <(list_accounts_for_env "$env")
955
+
956
+ if [[ "$found" -eq 0 ]]; then
957
+ metrics="$(collect_profile_metrics_tsv "-" "$home/auth.json" "$home" || true)"
958
+ if [[ -n "$metrics" ]]; then
959
+ IFS=$'\t' read -r email plan usage_5h usage_weekly last_activity source <<< "$metrics"
960
+ fi
961
+ email="${email:--}"
962
+ plan="${plan:-unknown}"
963
+ usage_5h="${usage_5h:--}"
964
+ usage_weekly="${usage_weekly:--}"
965
+ last_activity="${last_activity:--}"
966
+ source="${source:-local}"
967
+ [[ "$source" == "api" || "$source" == "local" ]] || source="local"
968
+ last_activity="${last_activity} (${source})"
969
+ printf '%-12s %-30s %-44s %-10s %-16s %-18s %s\n' "$env" "-" "$email" "$plan" "$usage_5h" "$usage_weekly" "$last_activity"
970
+ fi
971
+ done < <(list_env_names | sort -u)
972
+ }
973
+
974
+ cmd_add() {
975
+ local account="$1"
976
+ local env
977
+ env="$(read_current_env cli || true)"
978
+ cmd_account_add "$account" "$env"
979
+ }
980
+
981
+ cmd_remove() {
982
+ local account="$1"
983
+ local force="$2"
984
+ local env
985
+ env="$(read_current_env cli || true)"
986
+ cmd_account_remove "$account" "$env" "$force"
987
+ }
988
+
989
+ cmd_import_default() {
990
+ local env="$1"
991
+ local with_auth="$2"
992
+ local force="$3"
993
+
994
+ validate_env_name "$env"
995
+ [[ "$env" != "$DEFAULT_ENV_NAME" ]] || err "cannot import into reserved env '$DEFAULT_ENV_NAME'"
996
+
997
+ if env_exists "$env"; then
998
+ if [[ "$force" != "true" ]]; then
999
+ err "env '$env' already exists. use --force to overwrite"
1000
+ fi
1001
+ rm -rf -- "$(env_root_path "$env")" "$ACCOUNTS_DIR/$env"
1002
+ fi
1003
+
1004
+ cmd_env_create "$env" "from-default" ""
1005
+ if [[ "$with_auth" == "true" && -f "$DEFAULT_HOME/auth.json" ]]; then
1006
+ ensure_account_slot "$env" "$DEFAULT_ACCOUNT_NAME"
1007
+ copy_auth_file "$DEFAULT_HOME/auth.json" "$(account_auth_path "$env" "$DEFAULT_ACCOUNT_NAME")"
1008
+ fi
1009
+
1010
+ log_event INFO "import_default_as_env env=$env with_auth=$with_auth force=$force"
1011
+ echo "Imported default data to env: $env"
1012
+ }
1013
+
1014
+ cmd_use_compat() {
1015
+ local account="$1"
1016
+ local sync_mode="${2:-false}"
1017
+ local env
1018
+ env="$(read_current_env cli || true)"
1019
+ cmd_account_use "$account" "$env" "cli" "$sync_mode"
1020
+ }
1021
+
1022
+ cmd_switch_compat() {
1023
+ cmd_use_compat "$1" "${2:-false}"
521
1024
  }
522
1025
 
523
1026
  should_launch_codex() {
@@ -540,17 +1043,17 @@ should_launch_codex() {
540
1043
 
541
1044
  cmd_use_and_maybe_launch() {
542
1045
  local mode_cmd="$1"
543
- local profile="$2"
1046
+ local account="$2"
544
1047
  local sync_mode="${3:-false}"
545
1048
  local launch_mode="${4:-auto}"
546
1049
  shift 4 || true
547
1050
 
548
- with_lock "$mode_cmd" "$profile" "$sync_mode"
1051
+ with_lock "$mode_cmd" "$account" "$sync_mode"
549
1052
 
550
- echo "Switched CLI profile to: $profile"
1053
+ echo "Switched CLI account to: $(read_current_env cli || true)/$(read_current_account cli || true)"
551
1054
  if should_launch_codex "$launch_mode"; then
552
- echo "Launching codex with profile: $profile"
553
- CODEX_HOME="$(profile_path "$profile")" command codex "$@"
1055
+ echo "Launching codex with env/account: $(read_current_env cli || true)/$(read_current_account cli || true)"
1056
+ run_codex_for_target cli "$@"
554
1057
  return
555
1058
  fi
556
1059
 
@@ -558,21 +1061,21 @@ cmd_use_and_maybe_launch() {
558
1061
  echo "Auto launch skipped (non-interactive shell). Use --launch to force start."
559
1062
  fi
560
1063
  echo "Run in current shell if needed:"
561
- echo " export CODEX_HOME='$(profile_path "$profile")'"
1064
+ echo " export CODEX_HOME='$(target_home_path cli)'"
562
1065
  }
563
1066
 
564
1067
  cmd_current() {
565
1068
  local target="${1:-all}"
566
1069
  case "$target" in
567
1070
  cli)
568
- read_current cli || true
1071
+ echo "$(read_current_env cli || true)/$(read_current_account cli || true)"
569
1072
  ;;
570
1073
  app)
571
- read_current app || true
1074
+ echo "$(read_current_env app || true)/$(read_current_account app || true)"
572
1075
  ;;
573
1076
  all)
574
- echo "cli: $(read_current cli || true)"
575
- echo "app: $(read_current app || true)"
1077
+ echo "cli: $(read_current_env cli || true)/$(read_current_account cli || true)"
1078
+ echo "app: $(read_current_env app || true)/$(read_current_account app || true)"
576
1079
  ;;
577
1080
  *)
578
1081
  err "invalid target '$target' (use cli|app)"
@@ -581,31 +1084,31 @@ cmd_current() {
581
1084
  }
582
1085
 
583
1086
  cmd_status() {
584
- local cli_profile app_profile
585
- local cli_ptr_state app_ptr_state
586
- local cli_state app_state
587
- local exit_code
588
-
589
- cli_profile="$(read_current cli)" || cli_ptr_state=$?
590
- app_profile="$(read_current app)" || app_ptr_state=$?
1087
+ local cli_env app_env cli_account app_account
1088
+ local cli_ptr_state app_ptr_state cli_acc_state app_acc_state
1089
+ local cli_state app_state exit_code
1090
+
1091
+ cli_env="$(read_current_env cli)" || cli_ptr_state=$?
1092
+ app_env="$(read_current_env app)" || app_ptr_state=$?
1093
+ cli_account="$(read_current_account cli)" || cli_acc_state=$?
1094
+ app_account="$(read_current_account app)" || app_acc_state=$?
591
1095
  cli_ptr_state="${cli_ptr_state:-0}"
592
1096
  app_ptr_state="${app_ptr_state:-0}"
1097
+ cli_acc_state="${cli_acc_state:-0}"
1098
+ app_acc_state="${app_acc_state:-0}"
593
1099
 
594
- cli_state="$(login_state_for_profile "$cli_profile")"
595
- app_state="$(login_state_for_profile "$app_profile")"
1100
+ cli_state="$(login_state_for_target cli)"
1101
+ app_state="$(login_state_for_target app)"
596
1102
 
597
- echo "cli_current: $cli_profile"
598
- echo "app_current: $app_profile"
599
- echo "cli($cli_profile): $cli_state"
600
- echo "app($app_profile): $app_state"
1103
+ echo "cli_current: $cli_env/$cli_account"
1104
+ echo "app_current: $app_env/$app_account"
1105
+ echo "cli($cli_env/$cli_account): $cli_state"
1106
+ echo "app($app_env/$app_account): $app_state"
601
1107
 
602
1108
  exit_code=0
603
- if [[ "$cli_ptr_state" -eq 2 || "$app_ptr_state" -eq 2 ]]; then
1109
+ if [[ "$cli_ptr_state" -eq 2 || "$app_ptr_state" -eq 2 || "$cli_acc_state" -eq 2 || "$app_acc_state" -eq 2 ]]; then
604
1110
  echo "hint: pointer file corrupted, run: $SCRIPT_NAME recover" >&2
605
1111
  exit_code=2
606
- elif [[ "$cli_state" == "missing-profile" || "$app_state" == "missing-profile" ]]; then
607
- echo "hint: missing profile directory, run: $SCRIPT_NAME recover" >&2
608
- exit_code=2
609
1112
  elif [[ "$cli_state" != "logged-in" || "$app_state" != "logged-in" ]]; then
610
1113
  exit_code=1
611
1114
  fi
@@ -619,85 +1122,76 @@ cmd_exec() {
619
1122
  shift
620
1123
  fi
621
1124
  [[ "$#" -gt 0 ]] || err "missing codex args after --"
622
-
623
- local profile
624
- profile="$(read_current cli || true)"
625
- ensure_profile "$profile"
626
- CODEX_HOME="$(profile_path "$profile")" command codex "$@"
1125
+ run_codex_for_target cli "$@"
627
1126
  }
628
1127
 
629
1128
  cmd_login() {
630
- local profile="${1:-$(read_current cli || true)}"
1129
+ local account="${1:-$(read_current_account cli || true)}"
631
1130
  local sync_mode="${2:-false}"
632
- validate_profile_name "$profile"
633
- ensure_profile "$profile"
634
- if [[ "$sync_mode" == "true" ]]; then
635
- sync_default_to_profile "$profile"
636
- fi
637
- log_event INFO "cli_login profile=$profile sync=$sync_mode"
638
- run_codex_for_profile "$profile" login
1131
+ local env
1132
+ env="$(read_current_env cli || true)"
1133
+ cmd_account_login "$account" "$env" "cli" "$sync_mode"
639
1134
  }
640
1135
 
641
1136
  cmd_logout() {
642
- local profile="${1:-$(read_current cli || true)}"
643
- validate_profile_name "$profile"
644
- ensure_profile "$profile"
645
- log_event INFO "cli_logout profile=$profile"
646
- run_codex_for_profile "$profile" logout
647
- }
648
-
649
- cmd_env() {
650
- local profile="${1:-$(read_current cli || true)}"
651
- validate_profile_name "$profile"
652
- ensure_profile "$profile"
653
- echo "export CODEX_HOME='$(profile_path "$profile")'"
1137
+ local account="${1:-}"
1138
+ local env
1139
+ env="$(read_current_env cli || true)"
1140
+ cmd_account_logout "$account" "$env" "cli"
654
1141
  }
655
1142
 
656
1143
  cmd_app_open() {
657
- local profile="$1"
658
- shift
659
- local app_bin pid login_state
1144
+ local account="${1:-}"
1145
+ shift || true
1146
+ local env app_bin pid login_state use_account
1147
+
1148
+ env="$(read_current_env app || true)"
1149
+ use_account="${account:-$(read_current_account app || true)}"
1150
+ validate_account_name "$use_account"
1151
+ require_env_exists "$env"
1152
+ account_has_auth "$env" "$use_account" || err "account '$use_account' not logged in under env '$env'. run: $SCRIPT_NAME account login $use_account --env $env --target app"
1153
+
1154
+ set_current_env app "$env"
1155
+ set_current_account app "$use_account"
1156
+ ensure_target_auth_installed app
660
1157
 
661
- validate_profile_name "$profile"
662
- require_profile_exists "$profile"
663
- login_state="$(login_state_for_profile "$profile")"
1158
+ login_state="$(login_state_for_target app)"
664
1159
  if [[ "$login_state" != "logged-in" ]]; then
665
- err "profile '$profile' is not logged in. run: $SCRIPT_NAME login $profile"
1160
+ err "account '$use_account' in env '$env' is not logged in"
666
1161
  fi
667
1162
  app_bin="$(resolve_app_bin)"
668
1163
 
669
1164
  app_stop_managed >/dev/null
670
1165
 
671
1166
  mkdir -p "$(dirname "$APP_LOG")"
672
- nohup env CODEX_HOME="$(profile_path "$profile")" CODEX_SWITCHER_MANAGED=1 CODEX_SWITCHER_PROFILE="$profile" "$app_bin" "$@" >>"$APP_LOG" 2>&1 &
1167
+ nohup env CODEX_HOME="$(target_home_path app)" CODEX_SWITCHER_MANAGED=1 CODEX_SWITCHER_ENV="$env" CODEX_SWITCHER_ACCOUNT="$use_account" "$app_bin" "$@" >>"$APP_LOG" 2>&1 &
673
1168
  pid="$!"
674
1169
  printf '%s\n' "$pid" > "$APP_PID_FILE"
675
1170
  chmod 600 "$APP_PID_FILE" 2>/dev/null || true
676
1171
 
677
- set_current app "$profile"
678
- log_event INFO "app_open profile=$profile pid=$pid args=$*"
679
- echo "Opened Codex App with profile: $profile"
1172
+ log_event INFO "app_open env=$env account=$use_account pid=$pid args=$*"
1173
+ echo "Opened Codex App with: $env/$use_account"
680
1174
  echo "App log: $APP_LOG"
681
1175
  }
682
1176
 
683
1177
  cmd_app_use() {
684
- local profile="$1"
685
- shift
686
- cmd_app_open "$profile" "$@"
1178
+ local account="$1"
1179
+ shift || true
1180
+ cmd_app_open "$account" "$@"
687
1181
  }
688
1182
 
689
1183
  cmd_app_logout() {
690
- local profile="${1:-$(read_current app || true)}"
691
- validate_profile_name "$profile"
692
- ensure_profile "$profile"
693
- log_event INFO "app_logout profile=$profile"
694
- run_codex_for_profile "$profile" logout
1184
+ local account="${1:-}"
1185
+ local env
1186
+ env="$(read_current_env app || true)"
1187
+ cmd_account_logout "$account" "$env" "app"
695
1188
  }
696
1189
 
697
1190
  cmd_app_status() {
698
- local profile
699
- profile="$(read_current app || true)"
700
- echo "app_current: $profile"
1191
+ local env account
1192
+ env="$(read_current_env app || true)"
1193
+ account="$(read_current_account app || true)"
1194
+ echo "app_current: $env/$account"
701
1195
  if app_is_running; then
702
1196
  echo "app_process: running(pid=$(cat "$APP_PID_FILE"))"
703
1197
  return 0
@@ -801,44 +1295,34 @@ cmd_upgrade() {
801
1295
 
802
1296
  cmd_recover() {
803
1297
  local dry_run="${1:-false}"
804
- local cli_profile app_profile cli_ptr_state app_ptr_state first
805
-
806
- cli_profile="$(read_current cli)" || cli_ptr_state=$?
807
- app_profile="$(read_current app)" || app_ptr_state=$?
808
- cli_ptr_state="${cli_ptr_state:-0}"
809
- app_ptr_state="${app_ptr_state:-0}"
810
-
811
- if [[ ! -d "$(profile_path "$cli_profile")" ]]; then
812
- if first="$(first_profile_name)"; then
813
- cli_profile="$first"
814
- else
815
- cli_profile="$(default_profile_for cli)"
816
- ensure_profile "$cli_profile"
1298
+ local target env account
1299
+
1300
+ for target in cli app; do
1301
+ env="$(read_current_env "$target" || true)"
1302
+ account="$(read_current_account "$target" || true)"
1303
+ validate_env_name_noexit "$env" || env="$DEFAULT_ENV_NAME"
1304
+ if [[ "$env" != "$DEFAULT_ENV_NAME" ]] && [[ ! -d "$(env_home_path "$env")" ]]; then
1305
+ env="$DEFAULT_ENV_NAME"
1306
+ fi
1307
+ validate_name_noexit "$account" || account="$DEFAULT_ACCOUNT_NAME"
1308
+ if ! account_has_auth "$env" "$account"; then
1309
+ account="$DEFAULT_ACCOUNT_NAME"
817
1310
  fi
818
- cli_ptr_state=3
819
- fi
820
1311
 
821
- if [[ ! -d "$(profile_path "$app_profile")" ]]; then
822
- if [[ -d "$(profile_path "$cli_profile")" ]]; then
823
- app_profile="$cli_profile"
824
- elif first="$(first_profile_name)"; then
825
- app_profile="$first"
826
- else
827
- app_profile="$(default_profile_for app)"
828
- ensure_profile "$app_profile"
1312
+ if [[ "$dry_run" != "true" ]]; then
1313
+ set_current_env "$target" "$env"
1314
+ set_current_account "$target" "$account"
1315
+ ensure_target_auth_installed "$target"
829
1316
  fi
830
- app_ptr_state=3
831
- fi
1317
+
1318
+ echo "recover($target): $env/$account"
1319
+ done
832
1320
 
833
1321
  if [[ "$dry_run" == "true" ]]; then
834
- echo "recover(dry-run): cli=$cli_profile app=$app_profile"
835
1322
  return 0
836
1323
  fi
837
1324
 
838
- set_current cli "$cli_profile"
839
- set_current app "$app_profile"
840
- log_event INFO "recover cli=$cli_profile app=$app_profile"
841
- echo "Recovered pointers: cli=$cli_profile app=$app_profile"
1325
+ log_event INFO "recover cli=$(read_current_env cli || true)/$(read_current_account cli || true) app=$(read_current_env app || true)/$(read_current_account app || true)"
842
1326
  }
843
1327
 
844
1328
  scan_logs_for_secrets() {
@@ -857,39 +1341,53 @@ scan_logs_for_secrets() {
857
1341
  cmd_check() {
858
1342
  command -v codex >/dev/null 2>&1 || err "codex command not found in PATH"
859
1343
  resolve_app_bin >/dev/null
860
-
861
1344
  ensure_dirs
862
- local pstate
863
- read_current cli >/dev/null || pstate=$?
864
- if [[ "${pstate:-0}" -eq 2 ]]; then
865
- err "current_cli pointer corrupted; run: $SCRIPT_NAME recover"
1345
+
1346
+ local pstate=0
1347
+ read_current_env cli >/dev/null || pstate=$?
1348
+ if [[ "$pstate" -eq 2 ]]; then
1349
+ err "current_cli_env pointer corrupted; run: $SCRIPT_NAME recover"
866
1350
  fi
867
1351
  pstate=0
868
- read_current app >/dev/null || pstate=$?
1352
+ read_current_env app >/dev/null || pstate=$?
869
1353
  if [[ "$pstate" -eq 2 ]]; then
870
- err "current_app pointer corrupted; run: $SCRIPT_NAME recover"
1354
+ err "current_app_env pointer corrupted; run: $SCRIPT_NAME recover"
1355
+ fi
1356
+ pstate=0
1357
+ read_current_account cli >/dev/null || pstate=$?
1358
+ if [[ "$pstate" -eq 2 ]]; then
1359
+ err "current_cli_account pointer corrupted; run: $SCRIPT_NAME recover"
1360
+ fi
1361
+ pstate=0
1362
+ read_current_account app >/dev/null || pstate=$?
1363
+ if [[ "$pstate" -eq 2 ]]; then
1364
+ err "current_app_account pointer corrupted; run: $SCRIPT_NAME recover"
871
1365
  fi
872
1366
 
873
1367
  if ! scan_logs_for_secrets; then
874
1368
  err "log redaction check failed"
875
1369
  fi
876
1370
 
1371
+ echo "version: $(switcher_version)"
877
1372
  echo "check: ok"
878
1373
  }
879
1374
 
880
1375
  cmd_doctor() {
881
1376
  local fix="${1:-false}"
882
1377
  local issues=0
883
- local state_perm profiles_perm
1378
+ local state_perm envs_perm accounts_perm
884
1379
 
885
1380
  ensure_dirs
886
1381
  state_perm="$(perm_of "$STATE_DIR")"
887
- profiles_perm="$(perm_of "$PROFILES_DIR")"
1382
+ envs_perm="$(perm_of "$ENVS_DIR")"
1383
+ accounts_perm="$(perm_of "$ACCOUNTS_DIR")"
888
1384
 
889
1385
  echo "state_dir: $STATE_DIR (perm=$state_perm)"
890
- echo "profiles_dir: $PROFILES_DIR (perm=$profiles_perm)"
891
- echo "cli_current: $(read_current cli || true)"
892
- echo "app_current: $(read_current app || true)"
1386
+ echo "envs_dir: $ENVS_DIR (perm=$envs_perm)"
1387
+ echo "accounts_dir: $ACCOUNTS_DIR (perm=$accounts_perm)"
1388
+ echo "default_home: $DEFAULT_HOME (perm=$(perm_of "$DEFAULT_HOME"))"
1389
+ echo "cli_current: $(read_current_env cli || true)/$(read_current_account cli || true)"
1390
+ echo "app_current: $(read_current_env app || true)/$(read_current_account app || true)"
893
1391
 
894
1392
  if ! command -v codex >/dev/null 2>&1; then
895
1393
  echo "- codex in PATH: missing"
@@ -905,20 +1403,9 @@ cmd_doctor() {
905
1403
  issues=1
906
1404
  fi
907
1405
 
908
- if [[ "$state_perm" != "700" ]]; then
909
- echo "- state dir permission not 700"
910
- issues=1
911
- if [[ "$fix" == "true" ]]; then
912
- chmod 700 "$STATE_DIR" || true
913
- fi
914
- fi
915
-
916
- if [[ "$profiles_perm" != "700" ]]; then
917
- echo "- profiles dir permission not 700"
1406
+ if [[ "$state_perm" != "700" || "$envs_perm" != "700" || "$accounts_perm" != "700" ]]; then
918
1407
  issues=1
919
- if [[ "$fix" == "true" ]]; then
920
- chmod 700 "$PROFILES_DIR" || true
921
- fi
1408
+ echo "- directory permissions are not strict 700"
922
1409
  fi
923
1410
 
924
1411
  if ! scan_logs_for_secrets; then
@@ -927,23 +1414,9 @@ cmd_doctor() {
927
1414
  echo "- redaction check: ok"
928
1415
  fi
929
1416
 
930
- local pstate=0
931
- read_current cli >/dev/null || pstate=$?
932
- if [[ "$pstate" -eq 2 ]]; then
933
- echo "- current_cli pointer corrupted"
934
- issues=1
935
- fi
936
- pstate=0
937
- read_current app >/dev/null || pstate=$?
938
- if [[ "$pstate" -eq 2 ]]; then
939
- echo "- current_app pointer corrupted"
940
- issues=1
941
- fi
942
-
943
1417
  if [[ "$fix" == "true" ]]; then
1418
+ chmod 700 "$STATE_DIR" "$ENVS_DIR" "$ACCOUNTS_DIR" "$DEFAULT_HOME" 2>/dev/null || true
944
1419
  with_lock cmd_recover false
945
- chmod 700 "$STATE_DIR" "$PROFILES_DIR" 2>/dev/null || true
946
- find "$PROFILES_DIR" -mindepth 1 -maxdepth 1 -type d -exec chmod 700 {} \; 2>/dev/null || true
947
1420
  issues=0
948
1421
  if ! scan_logs_for_secrets; then
949
1422
  issues=1
@@ -974,12 +1447,265 @@ main() {
974
1447
  shift || true
975
1448
 
976
1449
  case "$cmd" in
1450
+ env)
1451
+ local sub="${1:-}"
1452
+ if [[ -z "$sub" ]]; then
1453
+ usage
1454
+ exit 1
1455
+ fi
1456
+ shift || true
1457
+ case "$sub" in
1458
+ list)
1459
+ [[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME env list"
1460
+ cmd_env_list
1461
+ ;;
1462
+ create)
1463
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME env create <env> [--empty|--from-default|--from-env <src>]"
1464
+ local env="$1"
1465
+ shift
1466
+ local mode="from-default"
1467
+ local src_env=""
1468
+ while [[ "$#" -gt 0 ]]; do
1469
+ case "$1" in
1470
+ --empty)
1471
+ mode="empty"
1472
+ ;;
1473
+ --from-default)
1474
+ mode="from-default"
1475
+ ;;
1476
+ --from-env)
1477
+ shift
1478
+ [[ "$#" -gt 0 ]] || err "missing source env after --from-env"
1479
+ mode="from-env"
1480
+ src_env="$1"
1481
+ ;;
1482
+ *)
1483
+ err "unknown option: $1"
1484
+ ;;
1485
+ esac
1486
+ shift
1487
+ done
1488
+ with_lock cmd_env_create "$env" "$mode" "$src_env"
1489
+ ;;
1490
+ use)
1491
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME env use <env> [--target cli|app|both]"
1492
+ local env="$1"
1493
+ shift
1494
+ local target="cli"
1495
+ while [[ "$#" -gt 0 ]]; do
1496
+ case "$1" in
1497
+ --target)
1498
+ shift
1499
+ [[ "$#" -gt 0 ]] || err "missing value after --target"
1500
+ target="$1"
1501
+ ;;
1502
+ *)
1503
+ err "unknown option: $1"
1504
+ ;;
1505
+ esac
1506
+ shift
1507
+ done
1508
+ with_lock cmd_env_use "$env" "$target"
1509
+ ;;
1510
+ remove)
1511
+ [[ "$#" -ge 1 && "$#" -le 2 ]] || err "usage: $SCRIPT_NAME env remove <env> [--force]"
1512
+ local env="$1"
1513
+ local force="false"
1514
+ if [[ "${2:-}" == "--force" ]]; then
1515
+ force="true"
1516
+ elif [[ -n "${2:-}" ]]; then
1517
+ err "unknown option: ${2:-}"
1518
+ fi
1519
+ with_lock cmd_env_remove "$env" "$force"
1520
+ ;;
1521
+ current)
1522
+ [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME env current [cli|app]"
1523
+ cmd_env_current "${1:-all}"
1524
+ ;;
1525
+ path)
1526
+ [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME env path [env]"
1527
+ cmd_env_path "${1:-}"
1528
+ ;;
1529
+ *)
1530
+ err "unknown env subcommand: $sub"
1531
+ ;;
1532
+ esac
1533
+ ;;
1534
+ account)
1535
+ local sub="${1:-}"
1536
+ [[ -n "$sub" ]] || err "usage: $SCRIPT_NAME account <list|add|remove|login|use|logout|current> ..."
1537
+ shift || true
1538
+ case "$sub" in
1539
+ list)
1540
+ local env="$(read_current_env cli || true)"
1541
+ while [[ "$#" -gt 0 ]]; do
1542
+ case "$1" in
1543
+ --env)
1544
+ shift
1545
+ [[ "$#" -gt 0 ]] || err "missing env after --env"
1546
+ env="$1"
1547
+ ;;
1548
+ *)
1549
+ err "unknown option: $1"
1550
+ ;;
1551
+ esac
1552
+ shift
1553
+ done
1554
+ cmd_account_list "$env"
1555
+ ;;
1556
+ add)
1557
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME account add <account> [--env <env>]"
1558
+ local account="$1"
1559
+ shift
1560
+ local env="$(read_current_env cli || true)"
1561
+ while [[ "$#" -gt 0 ]]; do
1562
+ case "$1" in
1563
+ --env)
1564
+ shift
1565
+ [[ "$#" -gt 0 ]] || err "missing env after --env"
1566
+ env="$1"
1567
+ ;;
1568
+ *)
1569
+ err "unknown option: $1"
1570
+ ;;
1571
+ esac
1572
+ shift
1573
+ done
1574
+ with_lock cmd_account_add "$account" "$env"
1575
+ ;;
1576
+ remove)
1577
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME account remove <account> [--env <env>] [--force]"
1578
+ local account="$1"
1579
+ shift
1580
+ local env="$(read_current_env cli || true)"
1581
+ local force="false"
1582
+ while [[ "$#" -gt 0 ]]; do
1583
+ case "$1" in
1584
+ --env)
1585
+ shift
1586
+ [[ "$#" -gt 0 ]] || err "missing env after --env"
1587
+ env="$1"
1588
+ ;;
1589
+ --force)
1590
+ force="true"
1591
+ ;;
1592
+ *)
1593
+ err "unknown option: $1"
1594
+ ;;
1595
+ esac
1596
+ shift
1597
+ done
1598
+ with_lock cmd_account_remove "$account" "$env" "$force"
1599
+ ;;
1600
+ login)
1601
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME account login <account> [--env <env>] [--target cli|app|both] [--sync|--no-sync]"
1602
+ local account="$1"
1603
+ shift
1604
+ local env="$(read_current_env cli || true)"
1605
+ local target="cli"
1606
+ local sync_mode="false"
1607
+ while [[ "$#" -gt 0 ]]; do
1608
+ case "$1" in
1609
+ --env)
1610
+ shift
1611
+ [[ "$#" -gt 0 ]] || err "missing env after --env"
1612
+ env="$1"
1613
+ ;;
1614
+ --target)
1615
+ shift
1616
+ [[ "$#" -gt 0 ]] || err "missing target after --target"
1617
+ target="$1"
1618
+ ;;
1619
+ --sync)
1620
+ sync_mode="true"
1621
+ ;;
1622
+ --no-sync)
1623
+ sync_mode="false"
1624
+ ;;
1625
+ *)
1626
+ err "unknown option: $1"
1627
+ ;;
1628
+ esac
1629
+ shift
1630
+ done
1631
+ with_lock cmd_account_login "$account" "$env" "$target" "$sync_mode"
1632
+ ;;
1633
+ use)
1634
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME account use <account> [--env <env>] [--target cli|app|both] [--sync|--no-sync]"
1635
+ local account="$1"
1636
+ shift
1637
+ local env="$(read_current_env cli || true)"
1638
+ local target="cli"
1639
+ local sync_mode="false"
1640
+ while [[ "$#" -gt 0 ]]; do
1641
+ case "$1" in
1642
+ --env)
1643
+ shift
1644
+ [[ "$#" -gt 0 ]] || err "missing env after --env"
1645
+ env="$1"
1646
+ ;;
1647
+ --target)
1648
+ shift
1649
+ [[ "$#" -gt 0 ]] || err "missing target after --target"
1650
+ target="$1"
1651
+ ;;
1652
+ --sync)
1653
+ sync_mode="true"
1654
+ ;;
1655
+ --no-sync)
1656
+ sync_mode="false"
1657
+ ;;
1658
+ *)
1659
+ err "unknown option: $1"
1660
+ ;;
1661
+ esac
1662
+ shift
1663
+ done
1664
+ with_lock cmd_account_use "$account" "$env" "$target" "$sync_mode"
1665
+ ;;
1666
+ logout)
1667
+ local account=""
1668
+ local env="$(read_current_env cli || true)"
1669
+ local target="cli"
1670
+ if [[ "$#" -gt 0 && "${1:-}" != --* ]]; then
1671
+ account="$1"
1672
+ shift
1673
+ fi
1674
+ while [[ "$#" -gt 0 ]]; do
1675
+ case "$1" in
1676
+ --env)
1677
+ shift
1678
+ [[ "$#" -gt 0 ]] || err "missing env after --env"
1679
+ env="$1"
1680
+ ;;
1681
+ --target)
1682
+ shift
1683
+ [[ "$#" -gt 0 ]] || err "missing target after --target"
1684
+ target="$1"
1685
+ ;;
1686
+ *)
1687
+ err "unknown option: $1"
1688
+ ;;
1689
+ esac
1690
+ shift
1691
+ done
1692
+ with_lock cmd_account_logout "$account" "$env" "$target"
1693
+ ;;
1694
+ current)
1695
+ [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME account current [cli|app]"
1696
+ cmd_account_current "${1:-all}"
1697
+ ;;
1698
+ *)
1699
+ err "unknown account subcommand: $sub"
1700
+ ;;
1701
+ esac
1702
+ ;;
977
1703
  add)
978
- [[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME add <profile>"
1704
+ [[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME add <account>"
979
1705
  with_lock cmd_add "$1"
980
1706
  ;;
981
1707
  remove)
982
- [[ "$#" -ge 1 && "$#" -le 2 ]] || err "usage: $SCRIPT_NAME remove <profile> [--force]"
1708
+ [[ "$#" -ge 1 && "$#" -le 2 ]] || err "usage: $SCRIPT_NAME remove <account> [--force]"
983
1709
  local force="false"
984
1710
  if [[ "${2:-}" == "--force" ]]; then
985
1711
  force="true"
@@ -993,8 +1719,8 @@ main() {
993
1719
  cmd_list
994
1720
  ;;
995
1721
  import-default)
996
- [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME import-default <profile> [--with-auth] [--force]"
997
- local profile="$1"
1722
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME import-default <env> [--with-auth] [--force]"
1723
+ local env="$1"
998
1724
  local with_auth="false"
999
1725
  local force="false"
1000
1726
  shift
@@ -1012,11 +1738,11 @@ main() {
1012
1738
  esac
1013
1739
  shift
1014
1740
  done
1015
- with_lock cmd_import_default "$profile" "$with_auth" "$force"
1741
+ with_lock cmd_import_default "$env" "$with_auth" "$force"
1016
1742
  ;;
1017
1743
  use)
1018
- [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME use <profile> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]"
1019
- local profile="$1"
1744
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME use <account> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]"
1745
+ local account="$1"
1020
1746
  shift
1021
1747
  local sync="false"
1022
1748
  local launch="auto"
@@ -1051,14 +1777,14 @@ main() {
1051
1777
  launch="true"
1052
1778
  fi
1053
1779
  if [[ "${#codex_args[@]}" -gt 0 ]]; then
1054
- cmd_use_and_maybe_launch cmd_use "$profile" "$sync" "$launch" "${codex_args[@]}"
1780
+ cmd_use_and_maybe_launch cmd_use_compat "$account" "$sync" "$launch" "${codex_args[@]}"
1055
1781
  else
1056
- cmd_use_and_maybe_launch cmd_use "$profile" "$sync" "$launch"
1782
+ cmd_use_and_maybe_launch cmd_use_compat "$account" "$sync" "$launch"
1057
1783
  fi
1058
1784
  ;;
1059
1785
  switch)
1060
- [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME switch <profile> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]"
1061
- local profile="$1"
1786
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME switch <account> [--sync|--no-sync] [--launch|--no-launch] [-- <codex args...>]"
1787
+ local account="$1"
1062
1788
  shift
1063
1789
  local sync="false"
1064
1790
  local launch="auto"
@@ -1093,9 +1819,9 @@ main() {
1093
1819
  launch="true"
1094
1820
  fi
1095
1821
  if [[ "${#codex_args[@]}" -gt 0 ]]; then
1096
- cmd_use_and_maybe_launch cmd_switch "$profile" "$sync" "$launch" "${codex_args[@]}"
1822
+ cmd_use_and_maybe_launch cmd_switch_compat "$account" "$sync" "$launch" "${codex_args[@]}"
1097
1823
  else
1098
- cmd_use_and_maybe_launch cmd_switch "$profile" "$sync" "$launch"
1824
+ cmd_use_and_maybe_launch cmd_switch_compat "$account" "$sync" "$launch"
1099
1825
  fi
1100
1826
  ;;
1101
1827
  current)
@@ -1110,8 +1836,8 @@ main() {
1110
1836
  cmd_exec "$@"
1111
1837
  ;;
1112
1838
  login)
1113
- [[ "$#" -le 2 ]] || err "usage: $SCRIPT_NAME login [profile] [--sync|--no-sync]"
1114
- local profile=""
1839
+ [[ "$#" -le 2 ]] || err "usage: $SCRIPT_NAME login [account] [--sync|--no-sync]"
1840
+ local account=""
1115
1841
  local sync="false"
1116
1842
  local arg
1117
1843
  for arg in "$@"; do
@@ -1123,44 +1849,43 @@ main() {
1123
1849
  sync="false"
1124
1850
  ;;
1125
1851
  *)
1126
- if [[ -z "$profile" ]]; then
1127
- profile="$arg"
1852
+ if [[ -z "$account" ]]; then
1853
+ account="$arg"
1128
1854
  else
1129
1855
  err "unknown option: $arg"
1130
1856
  fi
1131
1857
  ;;
1132
1858
  esac
1133
1859
  done
1134
- with_lock cmd_login "${profile:-}" "$sync"
1860
+ with_lock cmd_login "${account:-}" "$sync"
1135
1861
  ;;
1136
1862
  logout)
1137
- [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME logout [profile]"
1863
+ [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME logout [account]"
1138
1864
  with_lock cmd_logout "${1:-}"
1139
1865
  ;;
1140
- env)
1141
- [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME env [profile]"
1142
- cmd_env "${1:-}"
1143
- ;;
1144
1866
  app)
1145
1867
  local sub="${1:-}"
1146
1868
  [[ -n "$sub" ]] || err "usage: $SCRIPT_NAME app <open|use|logout|status|stop|current> ..."
1147
1869
  shift || true
1148
1870
  case "$sub" in
1149
1871
  open)
1150
- local profile="${1:-$(read_current app || true)}"
1151
- if [[ "$#" -gt 0 ]]; then shift; fi
1872
+ local account=""
1873
+ if [[ "$#" -gt 0 && "${1:-}" != "--" ]]; then
1874
+ account="$1"
1875
+ shift
1876
+ fi
1152
1877
  if [[ "${1:-}" == "--" ]]; then shift; fi
1153
- with_lock cmd_app_open "$profile" "$@"
1878
+ with_lock cmd_app_open "$account" "$@"
1154
1879
  ;;
1155
1880
  use)
1156
- [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME app use <profile> [-- <app args...>]"
1157
- local profile="$1"
1881
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME app use <account> [-- <app args...>]"
1882
+ local account="$1"
1158
1883
  shift
1159
1884
  if [[ "${1:-}" == "--" ]]; then shift; fi
1160
- with_lock cmd_app_use "$profile" "$@"
1885
+ with_lock cmd_app_use "$account" "$@"
1161
1886
  ;;
1162
1887
  logout)
1163
- [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME app logout [profile]"
1888
+ [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME app logout [account]"
1164
1889
  with_lock cmd_app_logout "${1:-}"
1165
1890
  ;;
1166
1891
  status)
@@ -1173,7 +1898,7 @@ main() {
1173
1898
  ;;
1174
1899
  current)
1175
1900
  [[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME app current"
1176
- cmd_current app
1901
+ echo "$(read_current_env app || true)/$(read_current_account app || true)"
1177
1902
  ;;
1178
1903
  *)
1179
1904
  err "unknown app subcommand: $sub"