codex-sw 0.3.0 → 0.3.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.3.1 - 2026-04-12
4
+
5
+ - Added strict App profile guard: `app open/use` now requires existing and logged-in profile.
6
+ - Added `import-default` to migrate data from `~/.codex` into a profile.
7
+ - Added `--sync|--no-sync` for `login`, `use`, and `switch` with overwrite sync (excluding `auth.json`).
8
+ - Updated `--help` and README docs for sync and migration workflows.
9
+
3
10
  ## 0.3.0 - 2026-04-12
4
11
 
5
12
  - Added `codex-sw` namespaced entrypoint (kept `codex-switcher` compatibility).
@@ -14,3 +21,6 @@
14
21
  - Added expanded smoke tests.
15
22
  - Added npm release helper: `npm run release:npm`.
16
23
  - Added `publishConfig` (public + npmjs registry) and publish guide docs.
24
+ - Added `import-default` command to migrate existing `~/.codex` data into a profile.
25
+ - Changed `app open/use` to require existing and logged-in profile (no silent auto-create).
26
+ - Added `login --sync` and `use/switch --sync` overwrite sync (all files except `auth.json`).
package/README.md CHANGED
@@ -25,13 +25,20 @@ codex-sw check
25
25
  codex-sw add work
26
26
  codex-sw add personal
27
27
 
28
- codex-sw use work
29
- codex-sw login
28
+ codex-sw use work --sync
29
+ codex-sw login --sync
30
30
  codex-sw exec -- login status
31
31
 
32
+ codex-sw switch personal --sync
32
33
  codex-sw app use personal
33
34
  ```
34
35
 
36
+ ## Sync options
37
+
38
+ - `login --sync`: sync `~/.codex` into the target profile (excluding `auth.json`).
39
+ - `use/switch --sync`: sync current CLI profile into target profile (excluding `auth.json`).
40
+ - `--no-sync`: keep strict isolation without data copy (default).
41
+
35
42
  ## Command reference
36
43
 
37
44
  See plugin guide:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-sw",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Profile-based account switcher for Codex CLI and Codex App using isolated CODEX_HOME directories.",
5
5
  "license": "MIT",
6
6
  "author": "wangxt",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-switcher",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Profile-based account switcher for Codex CLI and Codex App without modifying upstream source.",
5
5
  "author": {
6
6
  "name": "wangxt",
@@ -15,13 +15,14 @@ Profile-based account switcher for Codex CLI and Codex App without modifying ups
15
15
  codex-sw add <profile>
16
16
  codex-sw remove <profile> [--force]
17
17
  codex-sw list
18
- codex-sw use <profile>
19
- codex-sw switch <profile>
18
+ codex-sw import-default <profile> [--with-auth] [--force]
19
+ codex-sw use <profile> [--sync|--no-sync]
20
+ codex-sw switch <profile> [--sync|--no-sync]
20
21
  codex-sw current [cli|app]
21
22
  codex-sw status
22
23
 
23
24
  codex-sw exec -- <codex args...>
24
- codex-sw login [profile]
25
+ codex-sw login [profile] [--sync|--no-sync]
25
26
  codex-sw logout [profile]
26
27
  codex-sw env [profile]
27
28
 
@@ -43,17 +44,49 @@ codex-sw doctor [--fix]
43
44
  codex-sw add work
44
45
  codex-sw add personal
45
46
 
46
- codex-sw use work
47
- codex-sw login
47
+ codex-sw use work --sync
48
+ codex-sw login --sync
48
49
  codex-sw exec -- login status
49
50
 
51
+ codex-sw switch personal --sync
50
52
  codex-sw app use personal
51
53
  ```
52
54
 
55
+ ## Migrate Existing App/CLI Data
56
+
57
+ If your existing data is in `~/.codex`, import it into a profile first:
58
+
59
+ ```bash
60
+ codex-sw import-default work
61
+ ```
62
+
63
+ This copies records/projects/history but excludes `auth.json` by default.
64
+ If you want to carry login state too:
65
+
66
+ ```bash
67
+ codex-sw import-default work --with-auth
68
+ ```
69
+
70
+ ## Sync Behavior
71
+
72
+ - `login --sync`: overwrite sync from default `~/.codex` to target profile, excluding `auth.json`.
73
+ - `use/switch --sync`: overwrite sync from current CLI profile to target profile, excluding `auth.json`.
74
+ - `--no-sync`: explicit no-sync mode (default behavior).
75
+
76
+ Examples:
77
+
78
+ ```bash
79
+ codex-sw login work --sync
80
+ codex-sw switch personal --sync
81
+ codex-sw use work --no-sync
82
+ ```
83
+
53
84
  ## Notes
54
85
 
55
86
  - Codex App is single-instance on macOS; switching App profile requires restart.
56
87
  - `codex-sw app stop` only stops app instances started and tracked by `codex-sw`.
88
+ - `codex-sw app open/use <profile>` requires that profile already exists and is logged in.
89
+ - `--sync` uses overwrite strategy (not merge): source overwrites target for all files except `auth.json`.
57
90
  - `codex-sw status` exit codes:
58
91
  - `0`: both current profiles logged in
59
92
  - `1`: at least one current profile not logged in
@@ -17,13 +17,14 @@ Usage:
17
17
  codex-sw add <profile>
18
18
  codex-sw remove <profile> [--force]
19
19
  codex-sw list
20
- codex-sw use <profile>
21
- codex-sw switch <profile>
20
+ codex-sw import-default <profile> [--with-auth] [--force]
21
+ codex-sw use <profile> [--sync|--no-sync]
22
+ codex-sw switch <profile> [--sync|--no-sync]
22
23
  codex-sw current [cli|app]
23
24
  codex-sw status
24
25
 
25
26
  codex-sw exec -- <codex args...>
26
- codex-sw login [profile]
27
+ codex-sw login [profile] [--sync|--no-sync]
27
28
  codex-sw logout [profile]
28
29
  codex-sw env [profile]
29
30
 
@@ -42,6 +43,12 @@ Usage:
42
43
  Compatibility:
43
44
  codex-switcher <same-commands>
44
45
 
46
+ Sync behavior:
47
+ --sync enable overwrite sync (excluding auth.json)
48
+ - login --sync: ~/.codex -> target profile
49
+ - use/switch --sync: current CLI profile -> target profile
50
+ --no-sync disable sync explicitly (default)
51
+
45
52
  Environment overrides:
46
53
  CODEX_SWITCHER_STATE_DIR
47
54
  CODEX_SWITCHER_PROFILES_DIR
@@ -49,6 +56,7 @@ Environment overrides:
49
56
  CODEX_SWITCHER_APP_LOG
50
57
  CODEX_SWITCHER_SWITCH_LOG
51
58
  CODEX_SWITCHER_LOCK_WAIT_SECONDS
59
+ CODEX_SWITCHER_DEFAULT_HOME
52
60
  USAGE
53
61
  }
54
62
 
@@ -400,19 +408,116 @@ cmd_list() {
400
408
  done
401
409
  }
402
410
 
411
+ cmd_import_default() {
412
+ local profile="$1"
413
+ local with_auth="$2"
414
+ local force="$3"
415
+ local src dst
416
+
417
+ src="${CODEX_SWITCHER_DEFAULT_HOME:-$HOME/.codex}"
418
+ validate_profile_name "$profile"
419
+ [[ -d "$src" ]] || err "default codex home not found: $src"
420
+ ensure_profile "$profile"
421
+ dst="$(profile_path "$profile")"
422
+
423
+ if [[ "$force" == "true" ]]; then
424
+ find "$dst" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
425
+ else
426
+ if find "$dst" -mindepth 1 -maxdepth 1 | grep -q .; then
427
+ err "profile '$profile' is not empty. use --force to overwrite"
428
+ fi
429
+ fi
430
+
431
+ if command -v rsync >/dev/null 2>&1; then
432
+ if [[ "$with_auth" == "true" ]]; then
433
+ rsync -a "$src/" "$dst/"
434
+ else
435
+ rsync -a --exclude 'auth.json' "$src/" "$dst/"
436
+ fi
437
+ else
438
+ if [[ "$with_auth" == "true" ]]; then
439
+ (cd "$src" && tar cf - .) | (cd "$dst" && tar xpf -)
440
+ else
441
+ (cd "$src" && tar cf - --exclude ./auth.json .) | (cd "$dst" && tar xpf -)
442
+ fi
443
+ fi
444
+
445
+ chmod 700 "$dst" 2>/dev/null || true
446
+ log_event INFO "import_default profile=$profile src=$src with_auth=$with_auth force=$force"
447
+ echo "Imported default data to profile: $profile"
448
+ if [[ "$with_auth" != "true" ]]; then
449
+ echo "Auth not imported. Run: $SCRIPT_NAME login $profile"
450
+ fi
451
+ }
452
+
453
+ sync_overwrite_excluding_auth() {
454
+ local src="$1"
455
+ local dst="$2"
456
+
457
+ [[ -d "$src" ]] || err "sync source not found: $src"
458
+ mkdir -p "$dst"
459
+
460
+ if command -v rsync >/dev/null 2>&1; then
461
+ rsync -a --checksum --delete --exclude 'auth.json' "$src/" "$dst/"
462
+ else
463
+ find "$dst" -mindepth 1 -maxdepth 1 ! -name 'auth.json' -exec rm -rf -- {} + 2>/dev/null || true
464
+ (cd "$src" && tar cf - --exclude ./auth.json .) | (cd "$dst" && tar xpf -)
465
+ fi
466
+ chmod 700 "$dst" 2>/dev/null || true
467
+ }
468
+
469
+ sync_default_to_profile() {
470
+ local profile="$1"
471
+ local src dst
472
+ src="${CODEX_SWITCHER_DEFAULT_HOME:-$HOME/.codex}"
473
+ dst="$(profile_path "$profile")"
474
+ [[ -d "$src" ]] || err "default codex home not found: $src"
475
+ ensure_profile "$profile"
476
+ sync_overwrite_excluding_auth "$src" "$dst"
477
+ log_event INFO "sync_default profile=$profile src=$src"
478
+ echo "Synced default data to profile: $profile (auth.json excluded)"
479
+ }
480
+
481
+ sync_profile_to_profile() {
482
+ local from_profile="$1"
483
+ local to_profile="$2"
484
+ local src dst
485
+
486
+ if [[ "$from_profile" == "$to_profile" ]]; then
487
+ return 0
488
+ fi
489
+
490
+ src="$(profile_path "$from_profile")"
491
+ dst="$(profile_path "$to_profile")"
492
+ require_profile_exists "$from_profile"
493
+ ensure_profile "$to_profile"
494
+ sync_overwrite_excluding_auth "$src" "$dst"
495
+ log_event INFO "sync_profile from=$from_profile to=$to_profile"
496
+ echo "Synced profile data: $from_profile -> $to_profile (auth.json excluded)"
497
+ }
498
+
403
499
  cmd_use() {
404
500
  local profile="$1"
501
+ local sync_mode="${2:-false}"
502
+ local from_profile
405
503
  validate_profile_name "$profile"
504
+
505
+ if [[ "$sync_mode" == "true" ]]; then
506
+ from_profile="$(read_current cli || true)"
507
+ validate_profile_name "$from_profile"
508
+ sync_profile_to_profile "$from_profile" "$profile"
509
+ fi
510
+
406
511
  ensure_profile "$profile"
407
512
  set_current cli "$profile"
408
- log_event INFO "cli_use profile=$profile"
513
+ log_event INFO "cli_use profile=$profile sync=$sync_mode"
409
514
  echo "Switched CLI profile to: $profile"
410
515
  echo "Run in current shell if needed:"
411
516
  echo " export CODEX_HOME='$(profile_path "$profile")'"
412
517
  }
413
518
 
414
519
  cmd_switch() {
415
- cmd_use "$1"
520
+ cmd_use "$1" "${2:-false}"
416
521
  }
417
522
 
418
523
  cmd_current() {
@@ -482,9 +587,13 @@ cmd_exec() {
482
587
 
483
588
  cmd_login() {
484
589
  local profile="${1:-$(read_current cli || true)}"
590
+ local sync_mode="${2:-false}"
485
591
  validate_profile_name "$profile"
486
592
  ensure_profile "$profile"
487
- log_event INFO "cli_login profile=$profile"
593
+ if [[ "$sync_mode" == "true" ]]; then
594
+ sync_default_to_profile "$profile"
595
+ fi
596
+ log_event INFO "cli_login profile=$profile sync=$sync_mode"
488
597
  run_codex_for_profile "$profile" login
489
598
  }
490
599
 
@@ -506,10 +615,14 @@ cmd_env() {
506
615
  cmd_app_open() {
507
616
  local profile="$1"
508
617
  shift
509
- local app_bin pid
618
+ local app_bin pid login_state
510
619
 
511
620
  validate_profile_name "$profile"
512
- ensure_profile "$profile"
621
+ require_profile_exists "$profile"
622
+ login_state="$(login_state_for_profile "$profile")"
623
+ if [[ "$login_state" != "logged-in" ]]; then
624
+ err "profile '$profile' is not logged in. run: $SCRIPT_NAME login $profile"
625
+ fi
513
626
  app_bin="$(resolve_app_bin)"
514
627
 
515
628
  app_stop_managed >/dev/null
@@ -819,13 +932,53 @@ main() {
819
932
  [[ "$#" -eq 0 ]] || err "usage: $SCRIPT_NAME list"
820
933
  cmd_list
821
934
  ;;
935
+ import-default)
936
+ [[ "$#" -ge 1 ]] || err "usage: $SCRIPT_NAME import-default <profile> [--with-auth] [--force]"
937
+ local profile="$1"
938
+ local with_auth="false"
939
+ local force="false"
940
+ shift
941
+ while [[ "$#" -gt 0 ]]; do
942
+ case "$1" in
943
+ --with-auth)
944
+ with_auth="true"
945
+ ;;
946
+ --force)
947
+ force="true"
948
+ ;;
949
+ *)
950
+ err "unknown option: $1"
951
+ ;;
952
+ esac
953
+ shift
954
+ done
955
+ with_lock cmd_import_default "$profile" "$with_auth" "$force"
956
+ ;;
822
957
  use)
823
- [[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME use <profile>"
824
- with_lock cmd_use "$1"
958
+ [[ "$#" -ge 1 && "$#" -le 2 ]] || err "usage: $SCRIPT_NAME use <profile> [--sync|--no-sync]"
959
+ local profile="$1"
960
+ local sync="false"
961
+ if [[ "${2:-}" == "--sync" ]]; then
962
+ sync="true"
963
+ elif [[ "${2:-}" == "--no-sync" || -z "${2:-}" ]]; then
964
+ :
965
+ else
966
+ err "unknown option: ${2:-}"
967
+ fi
968
+ with_lock cmd_use "$profile" "$sync"
825
969
  ;;
826
970
  switch)
827
- [[ "$#" -eq 1 ]] || err "usage: $SCRIPT_NAME switch <profile>"
828
- with_lock cmd_switch "$1"
971
+ [[ "$#" -ge 1 && "$#" -le 2 ]] || err "usage: $SCRIPT_NAME switch <profile> [--sync|--no-sync]"
972
+ local profile="$1"
973
+ local sync="false"
974
+ if [[ "${2:-}" == "--sync" ]]; then
975
+ sync="true"
976
+ elif [[ "${2:-}" == "--no-sync" || -z "${2:-}" ]]; then
977
+ :
978
+ else
979
+ err "unknown option: ${2:-}"
980
+ fi
981
+ with_lock cmd_switch "$profile" "$sync"
829
982
  ;;
830
983
  current)
831
984
  [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME current [cli|app]"
@@ -839,8 +992,28 @@ main() {
839
992
  cmd_exec "$@"
840
993
  ;;
841
994
  login)
842
- [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME login [profile]"
843
- with_lock cmd_login "${1:-}"
995
+ [[ "$#" -le 2 ]] || err "usage: $SCRIPT_NAME login [profile] [--sync|--no-sync]"
996
+ local profile=""
997
+ local sync="false"
998
+ local arg
999
+ for arg in "$@"; do
1000
+ case "$arg" in
1001
+ --sync)
1002
+ sync="true"
1003
+ ;;
1004
+ --no-sync)
1005
+ sync="false"
1006
+ ;;
1007
+ *)
1008
+ if [[ -z "$profile" ]]; then
1009
+ profile="$arg"
1010
+ else
1011
+ err "unknown option: $arg"
1012
+ fi
1013
+ ;;
1014
+ esac
1015
+ done
1016
+ with_lock cmd_login "${profile:-}" "$sync"
844
1017
  ;;
845
1018
  logout)
846
1019
  [[ "$#" -le 1 ]] || err "usage: $SCRIPT_NAME logout [profile]"
@@ -54,6 +54,12 @@ export CODEX_SWITCHER_STATE_DIR="$STATE"
54
54
  export CODEX_SWITCHER_PROFILES_DIR="$PROFILES"
55
55
  export CODEX_SWITCHER_APP_BIN="$BIN/fake-codex-app"
56
56
  export CODEX_SWITCHER_LOCK_WAIT_SECONDS=2
57
+ export CODEX_SWITCHER_DEFAULT_HOME="$TMPBASE/default-home"
58
+
59
+ mkdir -p "$CODEX_SWITCHER_DEFAULT_HOME/memories"
60
+ echo '{"auth_mode":"chatgpt"}' > "$CODEX_SWITCHER_DEFAULT_HOME/auth.json"
61
+ echo '{"projects":["demo"]}' > "$CODEX_SWITCHER_DEFAULT_HOME/state_5.sqlite"
62
+ echo '{"memo":"persist"}' > "$CODEX_SWITCHER_DEFAULT_HOME/memories/demo.json"
57
63
 
58
64
  check_out="$("$SW" check)"
59
65
  echo "$check_out" | grep -q "check: ok"
@@ -66,8 +72,54 @@ echo "$init_out" | grep -q "\[dry-run\]"
66
72
  [[ "$("$SW" current cli)" == "personal" ]]
67
73
 
68
74
  "$SW" login personal
69
- "$SW" app use work
75
+ "$SW" login sync-login --sync
76
+ [[ -f "$PROFILES/sync-login/state_5.sqlite" ]]
77
+ [[ -f "$PROFILES/sync-login/memories/demo.json" ]]
78
+ [[ -f "$PROFILES/sync-login/auth.json" ]]
79
+
80
+ echo '{"auth_mode":"api_key","owner":"personal"}' > "$PROFILES/personal/auth.json"
81
+ echo '{"auth_mode":"api_key","owner":"work"}' > "$PROFILES/work/auth.json"
82
+ echo "from-personal-newer" > "$PROFILES/personal/history.jsonl"
83
+ echo "from-work-older-baseline" > "$PROFILES/work/history.jsonl"
84
+ "$SW" switch work --sync
85
+ [[ "$("$SW" current cli)" == "work" ]]
86
+ grep -q "from-personal-newer" "$PROFILES/work/history.jsonl"
87
+ grep -q '"owner":"work"' "$PROFILES/work/auth.json"
88
+ grep -q '"owner":"personal"' "$PROFILES/personal/auth.json"
89
+
90
+ "$SW" import-default imported
91
+ [[ -f "$PROFILES/imported/state_5.sqlite" ]]
92
+ [[ -f "$PROFILES/imported/memories/demo.json" ]]
93
+ [[ ! -f "$PROFILES/imported/auth.json" ]]
94
+ set +e
95
+ CODEX_HOME="$PROFILES/imported" codex login status >/tmp/codex_sw_imported_login 2>&1
96
+ imported_login_rc=$?
97
+ set -e
98
+ [[ "$imported_login_rc" -ne 0 ]]
99
+
100
+ "$SW" import-default imported-auth --with-auth
101
+ [[ -f "$PROFILES/imported-auth/auth.json" ]]
102
+ CODEX_HOME="$PROFILES/imported-auth" codex login status >/tmp/codex_sw_imported_auth_login 2>&1
103
+ [[ "$?" -eq 0 ]]
104
+
105
+ "$SW" logout work
106
+
107
+ set +e
108
+ "$SW" app open ghost >/tmp/codex_sw_app_open_missing 2>&1
109
+ app_open_missing_rc=$?
110
+ set -e
111
+ [[ "$app_open_missing_rc" -ne 0 ]]
112
+ grep -q "profile 'ghost' not found" /tmp/codex_sw_app_open_missing
113
+
114
+ set +e
115
+ "$SW" app use work >/tmp/codex_sw_app_use_unauthed 2>&1
116
+ app_use_unauthed_rc=$?
117
+ set -e
118
+ [[ "$app_use_unauthed_rc" -ne 0 ]]
119
+ grep -q "profile 'work' is not logged in" /tmp/codex_sw_app_use_unauthed
120
+
70
121
  "$SW" login work
122
+ "$SW" app use work
71
123
  [[ "$("$SW" app current)" == "work" ]]
72
124
 
73
125
  set +e
@@ -75,16 +127,16 @@ set +e
75
127
  status_rc=$?
76
128
  set -e
77
129
  [[ "$status_rc" -eq 0 ]]
78
- grep -q "cli(personal): logged-in" /tmp/codex_sw_status_1
130
+ grep -q "cli(work): logged-in" /tmp/codex_sw_status_1
79
131
  grep -q "app(work): logged-in" /tmp/codex_sw_status_1
80
132
 
81
- "$SW" logout personal
133
+ "$SW" logout work
82
134
  set +e
83
135
  "$SW" status >/tmp/codex_sw_status_2
84
136
  status_rc=$?
85
137
  set -e
86
138
  [[ "$status_rc" -eq 1 ]]
87
- grep -q "cli(personal): not-logged-in" /tmp/codex_sw_status_2
139
+ grep -q "cli(work): not-logged-in" /tmp/codex_sw_status_2
88
140
 
89
141
  "$SW" app status >/tmp/codex_sw_app_status_1
90
142
  [[ "$?" -eq 0 ]]