Last active
May 15, 2026 08:31
-
-
Save benhook1013/d18cd883e3b00837d7c55d8d4223b0cc to your computer and use it in GitHub Desktop.
WSL Codex/T3 profile switching helper and bash completion
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| umask 077 | |
| # Generated local config. | |
| CONFIG_FILE="${CODEX_PROFILES_CONFIG_FILE:-$HOME/.codex-profiles/config.env}" | |
| if [[ ! -f "$CONFIG_FILE" ]]; then | |
| mkdir -p "$(dirname "$CONFIG_FILE")" | |
| cat > "$CONFIG_FILE" <<'EOF' | |
| # codex-profiles local config | |
| # | |
| # Uncomment and edit any values you want to override for this machine. | |
| # | |
| # CODEX_PROFILES_REPO_DIR="$HOME/src/FireMUD" | |
| # CODEX_PROFILES_LIVE_HOME="$HOME/.codex" | |
| # CODEX_PROFILES_DIR="$HOME/.codex-profiles" | |
| # CODEX_PROFILES_BACKUPS_DIR="$HOME/.codex/backups" | |
| # CODEX_PROFILES_T3CODE_REPO="$HOME/src/t3code" | |
| # CODEX_PROFILES_T3_BIN="npx" | |
| # CODEX_PROFILES_T3_SUBCOMMAND="t3" | |
| # CODEX_PROFILES_DEFAULT_ORDER="personal,work,alt" | |
| # CODEX_PROFILES_T3_SETTINGS_PATHS="$HOME/.t3/userdata/settings.json:$HOME/.t3/dev/settings.json" | |
| EOF | |
| chmod 600 "$CONFIG_FILE" | |
| fi | |
| # shellcheck disable=SC1090 | |
| source "$CONFIG_FILE" | |
| # Configurable defaults. | |
| REPO_DIR="${CODEX_PROFILES_REPO_DIR:-$HOME/src/FireMUD}" | |
| LIVE_CODEX_HOME="${CODEX_PROFILES_LIVE_HOME:-$HOME/.codex}" | |
| PROFILES_DIR="${CODEX_PROFILES_DIR:-$HOME/.codex-profiles}" | |
| BACKUPS_DIR="${CODEX_PROFILES_BACKUPS_DIR:-$LIVE_CODEX_HOME/backups}" | |
| LOCAL_T3CODE_REPO="${CODEX_PROFILES_T3CODE_REPO:-$HOME/src/t3code}" | |
| PID_FILE="$LIVE_CODEX_HOME/t3.pid" | |
| LOG_FILE="$LIVE_CODEX_HOME/t3.log" | |
| PROFILE_ORDER_FILE="$PROFILES_DIR/.order" | |
| DISCOVER_CACHE_FILE="$LIVE_CODEX_HOME/.discover-cache" | |
| T3_COMMAND=("${CODEX_PROFILES_T3_BIN:-npx}" "${CODEX_PROFILES_T3_SUBCOMMAND:-t3}") | |
| DEFAULT_PROFILE_ORDER_CSV="${CODEX_PROFILES_DEFAULT_ORDER:-}" | |
| DEFAULT_T3_SETTINGS_PATHS="${HOME}/.t3/userdata/settings.json:${HOME}/.t3/dev/settings.json" | |
| T3_SETTINGS_PATHS_RAW="${CODEX_PROFILES_T3_SETTINGS_PATHS:-$DEFAULT_T3_SETTINGS_PATHS}" | |
| declare -a T3_SETTINGS_PATHS=() | |
| DRY_RUN=0 | |
| USE_LOCAL_T3CODE=0 | |
| USE_LOCAL_T3CODE_DEV=0 | |
| DEV_SERVER_PID_FILE="$LIVE_CODEX_HOME/t3-server.pid" | |
| DEV_WEB_PID_FILE="$LIVE_CODEX_HOME/t3-web.pid" | |
| DEV_SERVER_LOG_FILE="$LIVE_CODEX_HOME/t3-server.log" | |
| DEV_WEB_LOG_FILE="$LIVE_CODEX_HOME/t3-web.log" | |
| LOCAL_T3CODE_DEV_SERVER_PORT="${CODEX_PROFILES_T3CODE_DEV_SERVER_PORT:-13773}" | |
| LOCAL_T3CODE_DEV_WEB_PORT="${CODEX_PROFILES_T3CODE_DEV_WEB_PORT:-5733}" | |
| usage() { | |
| cat <<EOF | |
| Usage: codex-profiles [--dry-run] [--local-t3code] [--local-t3code-dev] <command> [args] | |
| Commands: | |
| discover | |
| Show likely auth.json candidates, and auto-import matches for existing profiles. | |
| inspect <path-to-auth.json> | |
| Show safe metadata for one auth.json file. | |
| import <discover-number> [name] | |
| Import from the last discover result when discover did not auto-import it. | |
| import <name> <path-to-auth.json> | |
| Copy an auth file into \$HOME/.codex-profiles/<name>/auth.json. | |
| list | |
| Show registered profile names, ordinals, import status, and account ids. | |
| current | |
| Show the currently logged-in live CLI profile and auth metadata. | |
| use <name-or-ordinal> | |
| Stop related T3/Codex processes, back up live auth, and activate a profile. | |
| start <name-or-ordinal> | |
| Activate a profile, then launch plain "npx t3" detached with nohup. | |
| serve <name-or-ordinal> | |
| Activate a profile, then launch plain "npx t3" in the foreground. | |
| t3-start | |
| Launch T3 detached without activating or switching any live Codex profile first. | |
| t3-serve | |
| Launch T3 in the foreground without activating or switching any live Codex profile first. | |
| sync-t3-accounts [name-or-ordinal ...] | |
| Write ordered profile home directories into T3's CODEX_HOME paths setting. | |
| With no args, sync every registered profile that has auth.json. | |
| import-t3-accounts [name-or-ordinal ...] | |
| Alias for sync-t3-accounts. Import the known Codex profile homes into | |
| T3 Code's Codex account settings. | |
| stop | |
| Stop related T3/Codex processes before switching or restart. | |
| help | |
| Show this help. | |
| Environment overrides: | |
| CODEX_PROFILES_REPO_DIR | |
| CODEX_PROFILES_LIVE_HOME | |
| CODEX_PROFILES_DIR | |
| CODEX_PROFILES_T3CODE_REPO | |
| CODEX_PROFILES_T3_BIN | |
| CODEX_PROFILES_T3_SUBCOMMAND | |
| CODEX_PROFILES_T3_SETTINGS_PATHS | |
| Flags: | |
| --dry-run | |
| Show the commands that would run. | |
| --local-t3code | |
| For start/serve, launch the local T3 Code checkout at | |
| CODEX_PROFILES_T3CODE_REPO instead of the installed "t3" command. | |
| --local-t3code-dev | |
| For start/serve, launch the local T3 Code checkout in dev mode | |
| against the target repo, with non-minified React errors. | |
| EOF | |
| } | |
| log() { | |
| printf '%s\n' "$*" | |
| } | |
| die() { | |
| printf 'Error: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| run_cmd() { | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] ' | |
| printf '%q ' "$@" | |
| printf '\n' | |
| else | |
| "$@" | |
| fi | |
| } | |
| ensure_layout() { | |
| run_cmd mkdir -p "$LIVE_CODEX_HOME" "$PROFILES_DIR" "$BACKUPS_DIR" | |
| ensure_profile_order_file | |
| populate_t3_settings_paths | |
| } | |
| ensure_profile_order_file() { | |
| [[ -f "$PROFILE_ORDER_FILE" ]] && return 0 | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] create profile order file %q with defaults\n' "$PROFILE_ORDER_FILE" | |
| return 0 | |
| fi | |
| : > "$PROFILE_ORDER_FILE" | |
| if [[ -n "$DEFAULT_PROFILE_ORDER_CSV" ]]; then | |
| local old_ifs="$IFS" name | |
| IFS=',' | |
| for name in $DEFAULT_PROFILE_ORDER_CSV; do | |
| [[ -n "$name" ]] || continue | |
| printf '%s\n' "$name" >> "$PROFILE_ORDER_FILE" | |
| done | |
| IFS="$old_ifs" | |
| fi | |
| } | |
| populate_t3_settings_paths() { | |
| local raw_path path | |
| local old_ifs="$IFS" | |
| IFS=':' | |
| read -r -a raw_paths <<<"$T3_SETTINGS_PATHS_RAW" | |
| IFS="$old_ifs" | |
| T3_SETTINGS_PATHS=() | |
| for raw_path in "${raw_paths[@]}"; do | |
| path="${raw_path/#\~/$HOME}" | |
| [[ -n "$path" ]] || continue | |
| T3_SETTINGS_PATHS+=("$path") | |
| done | |
| } | |
| timestamp() { | |
| date '+%Y%m%d-%H%M%S' | |
| } | |
| terminal_columns() { | |
| local cols | |
| cols="$(tput cols 2>/dev/null || true)" | |
| if [[ ! "$cols" =~ ^[0-9]+$ ]] || (( cols < 40 )); then | |
| cols=80 | |
| fi | |
| printf '%s\n' "$cols" | |
| } | |
| truncate_cell() { | |
| local text="$1" | |
| local width="$2" | |
| local len=${#text} | |
| if (( width <= 0 )); then | |
| printf '' | |
| return 0 | |
| fi | |
| if (( len <= width )); then | |
| printf '%s' "$text" | |
| return 0 | |
| fi | |
| if (( width <= 3 )); then | |
| printf '%.*s' "$width" "$text" | |
| return 0 | |
| fi | |
| printf '%s...' "${text:0:width-3}" | |
| } | |
| print_table_row() { | |
| local -n values_ref="$1" | |
| local -n widths_ref="$2" | |
| local i cell | |
| for ((i = 0; i < ${#values_ref[@]}; i++)); do | |
| cell="$(truncate_cell "${values_ref[$i]}" "${widths_ref[$i]}")" | |
| if (( i + 1 < ${#values_ref[@]} )); then | |
| printf '%-*s ' "${widths_ref[$i]}" "$cell" | |
| else | |
| printf '%s\n' "$cell" | |
| fi | |
| done | |
| } | |
| realpath_safe() { | |
| python3 - "$1" <<'PY' | |
| import os, sys | |
| print(os.path.realpath(os.path.expanduser(sys.argv[1]))) | |
| PY | |
| } | |
| profile_auth_path() { | |
| local name="$1" | |
| printf '%s/%s/auth.json\n' "$PROFILES_DIR" "$name" | |
| } | |
| require_profile_auth() { | |
| local name="$1" | |
| local path | |
| path="$(profile_auth_path "$name")" | |
| [[ -f "$path" ]] || die "profile '$name' does not have auth.json at $path" | |
| } | |
| profile_exists_in_order() { | |
| local name="$1" | |
| [[ -f "$PROFILE_ORDER_FILE" ]] || return 1 | |
| grep -Fxq "$name" "$PROFILE_ORDER_FILE" | |
| } | |
| append_profile_to_order() { | |
| local name="$1" | |
| profile_exists_in_order "$name" && return 0 | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] append profile %q to order file %q\n' "$name" "$PROFILE_ORDER_FILE" | |
| return 0 | |
| fi | |
| printf '%s\n' "$name" >> "$PROFILE_ORDER_FILE" | |
| } | |
| ordered_profiles() { | |
| if (( ! DRY_RUN )); then | |
| ensure_layout >/dev/null | |
| fi | |
| local -a ordered=() | |
| local -A seen=() | |
| local name dir | |
| while IFS= read -r name; do | |
| [[ -n "$name" ]] || continue | |
| [[ -n "${seen[$name]:-}" ]] && continue | |
| ordered+=("$name") | |
| seen["$name"]=1 | |
| done < "$PROFILE_ORDER_FILE" | |
| shopt -s nullglob | |
| for dir in "$PROFILES_DIR"/*; do | |
| [[ -d "$dir" ]] || continue | |
| name="$(basename "$dir")" | |
| [[ "$name" == .* ]] && continue | |
| [[ -n "${seen[$name]:-}" ]] && continue | |
| ordered+=("$name") | |
| seen["$name"]=1 | |
| done | |
| shopt -u nullglob | |
| printf '%s\n' "${ordered[@]}" | |
| } | |
| resolve_profile_spec() { | |
| local spec="$1" | |
| local -a names=() | |
| local index | |
| [[ -n "$spec" ]] || die "missing profile name or ordinal" | |
| mapfile -t names < <(ordered_profiles) | |
| if [[ "$spec" =~ ^[0-9]+$ ]]; then | |
| (( spec >= 1 )) || die "ordinal must be 1 or greater" | |
| index=$((spec - 1)) | |
| (( index < ${#names[@]} )) || die "ordinal $spec is out of range" | |
| printf '%s\n' "${names[$index]}" | |
| return 0 | |
| fi | |
| printf '%s\n' "$spec" | |
| } | |
| repo_realpath() { | |
| realpath_safe "$REPO_DIR" | |
| } | |
| live_realpath() { | |
| realpath_safe "$LIVE_CODEX_HOME" | |
| } | |
| collect_candidate_pids() { | |
| local repo_real live_real pid cmdline cwd | |
| repo_real="$(repo_realpath)" | |
| live_real="$(live_realpath)" | |
| declare -A seen=() | |
| local pid_path managed_pid | |
| for pid_path in "$PID_FILE" "$DEV_SERVER_PID_FILE" "$DEV_WEB_PID_FILE"; do | |
| if [[ -f "$pid_path" ]]; then | |
| managed_pid="$(tr -d '[:space:]' < "$pid_path" || true)" | |
| if [[ "$managed_pid" =~ ^[0-9]+$ ]] && [[ -d "/proc/$managed_pid" ]]; then | |
| seen["$managed_pid"]=1 | |
| fi | |
| fi | |
| done | |
| while read -r pid cmdline; do | |
| [[ -n "${pid:-}" ]] || continue | |
| [[ -d "/proc/$pid" ]] || continue | |
| [[ -n "$cmdline" ]] || continue | |
| if [[ ! "$cmdline" =~ (^|[[:space:]/])(npm|npx|node|bun|vite)([[:space:]]|$) ]] && [[ ! "$cmdline" =~ (^|[[:space:]/])codex([[:space:]]|$) ]] && [[ ! "$cmdline" =~ (^|[[:space:]/])t3([[:space:]]|$) ]]; then | |
| continue | |
| fi | |
| cwd="$(readlink -f "/proc/$pid/cwd" 2>/dev/null || true)" | |
| if tr '\0' '\n' < "/proc/$pid/environ" 2>/dev/null | grep -Fxq "CODEX_HOME=$live_real"; then | |
| seen["$pid"]=1 | |
| continue | |
| fi | |
| if [[ -n "$cwd" && ( "$cwd" == "$repo_real" || "$cwd" == "$LOCAL_T3CODE_REPO" || "$cwd" == "$LOCAL_T3CODE_REPO/apps/server" || "$cwd" == "$LOCAL_T3CODE_REPO/apps/web" ) ]] && { | |
| [[ "$cmdline" == *"npm exec t3"* ]] || | |
| [[ "$cmdline" == *"sh -c t3"* ]] || | |
| [[ "$cmdline" == *"/.bin/t3"* ]] || | |
| [[ "$cmdline" == "t3"* ]] || | |
| [[ "$cmdline" == *"vite"* ]] || | |
| [[ "$cmdline" == *"scripts/dev-runner.ts"* ]] || | |
| [[ "$cmdline" == *" codex app-server"* ]] || | |
| [[ "$cmdline" == "codex app-server"* ]] || | |
| [[ "$cmdline" == *"src/bin.ts"* ]]; | |
| }; then | |
| seen["$pid"]=1 | |
| continue | |
| fi | |
| done < <(ps -eo pid=,args=) | |
| (( ${#seen[@]} == 0 )) && return 0 | |
| printf '%s\n' "${!seen[@]}" | sort -n | |
| } | |
| stop_related_processes() { | |
| local -a pids=() | |
| mapfile -t pids < <(collect_candidate_pids) | |
| if (( ${#pids[@]} == 0 )); then | |
| log "No related T3/Codex processes found." | |
| return 0 | |
| fi | |
| log "Stopping related T3/Codex processes: ${pids[*]}" | |
| run_cmd kill "${pids[@]}" 2>/dev/null || true | |
| if (( ! DRY_RUN )); then | |
| local deadline remaining pid | |
| deadline=$((SECONDS + 10)) | |
| while (( SECONDS < deadline )); do | |
| remaining=0 | |
| for pid in "${pids[@]}"; do | |
| if kill -0 "$pid" 2>/dev/null; then | |
| remaining=1 | |
| break | |
| fi | |
| done | |
| (( remaining == 0 )) && break | |
| sleep 1 | |
| done | |
| local -a stubborn=() | |
| for pid in "${pids[@]}"; do | |
| if kill -0 "$pid" 2>/dev/null; then | |
| stubborn+=("$pid") | |
| fi | |
| done | |
| if (( ${#stubborn[@]} > 0 )); then | |
| log "Force-stopping stubborn processes: ${stubborn[*]}" | |
| kill -9 "${stubborn[@]}" 2>/dev/null || true | |
| fi | |
| rm -f "$PID_FILE" "$DEV_SERVER_PID_FILE" "$DEV_WEB_PID_FILE" | |
| fi | |
| } | |
| backup_live_auth() { | |
| local live_auth backup_path | |
| live_auth="$LIVE_CODEX_HOME/auth.json" | |
| [[ -f "$live_auth" ]] || return 0 | |
| backup_path="$BACKUPS_DIR/auth-$(timestamp).json" | |
| log "Backing up existing live auth to $backup_path" | |
| run_cmd cp -p "$live_auth" "$backup_path" | |
| } | |
| backup_profile_auth_if_present() { | |
| local profile_auth="$1" | |
| [[ -f "$profile_auth" ]] || return 0 | |
| local backup_path | |
| backup_path="${profile_auth%.json}.backup-$(timestamp).json" | |
| log "Backing up existing profile auth to $backup_path" | |
| run_cmd cp -p "$profile_auth" "$backup_path" | |
| } | |
| safe_label_for_auth() { | |
| local path="$1" | |
| python3 - "$path" <<'PY' | |
| import json, sys | |
| from pathlib import Path | |
| path = Path(sys.argv[1]) | |
| try: | |
| data = json.loads(path.read_text()) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| labels = [] | |
| def walk(obj, prefix=""): | |
| if isinstance(obj, dict): | |
| for key, value in obj.items(): | |
| child = f"{prefix}.{key}" if prefix else key | |
| if isinstance(value, (dict, list)): | |
| walk(value, child) | |
| elif isinstance(value, str): | |
| key_l = key.lower() | |
| if any(token in key_l for token in ("email", "user", "username", "account", "name", "org")) and not any(token in key_l for token in ("token", "secret", "key")): | |
| labels.append(f"{child}={value}") | |
| elif isinstance(obj, list): | |
| for index, value in enumerate(obj): | |
| walk(value, f"{prefix}[{index}]") | |
| walk(data) | |
| print(labels[0] if labels else "-") | |
| PY | |
| } | |
| account_id_for_auth() { | |
| local path="$1" | |
| python3 - "$path" <<'PY' | |
| import json, sys | |
| from pathlib import Path | |
| path = Path(sys.argv[1]) | |
| try: | |
| data = json.loads(path.read_text()) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| tokens = data.get("tokens") if isinstance(data, dict) else None | |
| if isinstance(tokens, dict) and isinstance(tokens.get("account_id"), str): | |
| print(tokens["account_id"]) | |
| else: | |
| print("-") | |
| PY | |
| } | |
| find_registered_profile_by_account_id() { | |
| local target_account_id="$1" | |
| local name auth_path current_account_id | |
| [[ -n "$target_account_id" && "$target_account_id" != "-" ]] || return 1 | |
| while IFS= read -r name; do | |
| [[ -n "$name" ]] || continue | |
| auth_path="$PROFILES_DIR/$name/auth.json" | |
| [[ -f "$auth_path" ]] || continue | |
| current_account_id="$(account_id_for_auth "$auth_path")" | |
| if [[ "$current_account_id" == "$target_account_id" ]]; then | |
| printf '%s\n' "$name" | |
| return 0 | |
| fi | |
| done < <(ordered_profiles) | |
| return 1 | |
| } | |
| show_auth_metadata() { | |
| local path="$1" | |
| [[ -e "$path" ]] || return 0 | |
| python3 - "$path" <<'PY' | |
| import hashlib, os, sys, time | |
| path = sys.argv[1] | |
| st = os.stat(path) | |
| digest = hashlib.sha256(open(path, "rb").read()).hexdigest()[:12] | |
| print(f"path={path}") | |
| print(f" mtime={time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(st.st_mtime))}") | |
| print(f" size={st.st_size}") | |
| print(f" sha256_prefix={digest}") | |
| PY | |
| printf ' label=%s\n' "$(safe_label_for_auth "$path")" | |
| } | |
| find_switch_profiles_file() { | |
| local wsl_path="$HOME/.vscode-server/data/User/globalStorage/woozy-masta.codex-switch/profiles.json" | |
| local local_code_path="$HOME/.config/Code/User/globalStorage/woozy-masta.codex-switch/profiles.json" | |
| local path | |
| if [[ -f "$wsl_path" ]]; then | |
| printf '%s\n' "$wsl_path" | |
| return 0 | |
| fi | |
| if [[ -f "$local_code_path" ]]; then | |
| printf '%s\n' "$local_code_path" | |
| return 0 | |
| fi | |
| shopt -s nullglob | |
| for path in /mnt/c/Users/*/AppData/Roaming/Code/User/globalStorage/woozy-masta.codex-switch/profiles.json; do | |
| [[ -f "$path" ]] || continue | |
| printf '%s\n' "$path" | |
| shopt -u nullglob | |
| return 0 | |
| done | |
| shopt -u nullglob | |
| return 1 | |
| } | |
| lookup_profile_hint_for_auth() { | |
| local path="$1" | |
| local account_id local_match profile_source="" | |
| account_id="$(account_id_for_auth "$path")" | |
| if local_match="$(find_registered_profile_by_account_id "$account_id")"; then | |
| printf '%s\n' "$local_match" | |
| return 0 | |
| fi | |
| if ! profile_source="$(find_switch_profiles_file)"; then | |
| printf '%s\n' "-" | |
| return 0 | |
| fi | |
| python3 - "$path" "$profile_source" "$PROFILE_ORDER_FILE" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| auth_path = Path(sys.argv[1]) | |
| profiles_path = Path(sys.argv[2]) | |
| order_path = Path(sys.argv[3]) | |
| try: | |
| auth = json.loads(auth_path.read_text()) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| account_id = ( | |
| auth.get("tokens", {}).get("account_id") | |
| if isinstance(auth, dict) else None | |
| ) | |
| if not account_id: | |
| print("-") | |
| raise SystemExit | |
| try: | |
| profiles = json.loads(profiles_path.read_text()).get("profiles", []) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| ordered = [] | |
| if order_path.exists(): | |
| ordered = [line.strip() for line in order_path.read_text().splitlines() if line.strip()] | |
| match_name = None | |
| for prof in profiles: | |
| if not isinstance(prof, dict): | |
| continue | |
| if prof.get("accountId") != account_id: | |
| continue | |
| raw_name = prof.get("name", "") | |
| lower_name = raw_name.lower() | |
| for ordered_name in ordered: | |
| ordered_lower = ordered_name.lower() | |
| if lower_name == ordered_lower or lower_name.startswith(ordered_lower + " "): | |
| match_name = ordered_name | |
| break | |
| if not match_name: | |
| match_name = raw_name or "-" | |
| break | |
| print(match_name if match_name else "-") | |
| PY | |
| } | |
| lookup_profile_hint_for_account_id() { | |
| local account_id="$1" | |
| local local_match profile_source="" | |
| if local_match="$(find_registered_profile_by_account_id "$account_id")"; then | |
| printf '%s\n' "$local_match" | |
| return 0 | |
| fi | |
| if ! profile_source="$(find_switch_profiles_file)"; then | |
| printf '%s\n' "-" | |
| return 0 | |
| fi | |
| python3 - "$account_id" "$profile_source" "$PROFILE_ORDER_FILE" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| account_id = sys.argv[1] | |
| profiles_path = Path(sys.argv[2]) | |
| order_path = Path(sys.argv[3]) | |
| try: | |
| profiles = json.loads(profiles_path.read_text()).get("profiles", []) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| ordered = [] | |
| if order_path.exists(): | |
| ordered = [line.strip() for line in order_path.read_text().splitlines() if line.strip()] | |
| match_name = None | |
| for prof in profiles: | |
| if not isinstance(prof, dict): | |
| continue | |
| if prof.get("accountId") != account_id: | |
| continue | |
| raw_name = prof.get("name", "") | |
| lower_name = raw_name.lower() | |
| for ordered_name in ordered: | |
| ordered_lower = ordered_name.lower() | |
| if lower_name == ordered_lower or lower_name.startswith(ordered_lower + " "): | |
| match_name = ordered_name | |
| break | |
| if not match_name: | |
| match_name = raw_name or "-" | |
| break | |
| print(match_name if match_name else "-") | |
| PY | |
| } | |
| lookup_profile_id_for_name() { | |
| local name="$1" | |
| local profile_source="" | |
| if ! profile_source="$(find_switch_profiles_file)"; then | |
| printf '%s\n' "-" | |
| return 0 | |
| fi | |
| python3 - "$name" "$profile_source" "$PROFILE_ORDER_FILE" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| target_name = sys.argv[1].strip().lower() | |
| profiles_path = Path(sys.argv[2]) | |
| try: | |
| profiles = json.loads(profiles_path.read_text()).get("profiles", []) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| for prof in profiles: | |
| if not isinstance(prof, dict): | |
| continue | |
| raw_name = str(prof.get("name", "")) | |
| lower_name = raw_name.lower() | |
| if lower_name == target_name or lower_name.startswith(target_name + " "): | |
| print(prof.get("accountId", "-")) | |
| raise SystemExit | |
| print("-") | |
| PY | |
| } | |
| write_discover_cache() { | |
| local index="$1" | |
| local path="$2" | |
| local hint="$3" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] cache discover result %s -> %s (%s)\n' "$index" "$path" "$hint" | |
| return 0 | |
| fi | |
| printf '%s\t%s\t%s\n' "$index" "$path" "$hint" >> "$DISCOVER_CACHE_FILE" | |
| } | |
| read_discover_cache_entry() { | |
| local target="$1" | |
| [[ -f "$DISCOVER_CACHE_FILE" ]] || die "no discover cache found; run 'cprof discover' first" | |
| python3 - "$DISCOVER_CACHE_FILE" "$target" <<'PY' | |
| import sys | |
| from pathlib import Path | |
| cache_path = Path(sys.argv[1]) | |
| target = sys.argv[2] | |
| for raw_line in cache_path.read_text().splitlines(): | |
| parts = raw_line.split("\t") | |
| if len(parts) != 3: | |
| continue | |
| if parts[0] == target: | |
| print("\t".join(parts)) | |
| raise SystemExit | |
| raise SystemExit(1) | |
| PY | |
| } | |
| import_profile_file() { | |
| local name="$1" | |
| local source="$2" | |
| local show_metadata="${3:-1}" | |
| [[ -f "$source" ]] || die "source auth file not found: $source" | |
| ensure_layout | |
| local dest_dir dest | |
| dest_dir="$PROFILES_DIR/$name" | |
| dest="$dest_dir/auth.json" | |
| run_cmd mkdir -p "$dest_dir" | |
| backup_profile_auth_if_present "$dest" | |
| log "Importing profile '$name' from $source" | |
| run_cmd cp -p "$source" "$dest" | |
| run_cmd chmod 600 "$dest" | |
| append_profile_to_order "$name" | |
| if [[ "$show_metadata" == "1" ]]; then | |
| show_auth_metadata "$dest" | |
| fi | |
| } | |
| ordered_profile_names_with_auth() { | |
| local name auth_path | |
| while IFS= read -r name; do | |
| [[ -n "$name" ]] || continue | |
| auth_path="$PROFILES_DIR/$name/auth.json" | |
| [[ -f "$auth_path" ]] || continue | |
| printf '%s\n' "$name" | |
| done < <(ordered_profiles) | |
| } | |
| profile_home_dir() { | |
| local name="$1" | |
| printf '%s/%s\n' "$PROFILES_DIR" "$name" | |
| } | |
| resolve_profiles_for_t3_sync() { | |
| local -a resolved=() | |
| local spec name | |
| if (($# == 0)); then | |
| mapfile -t resolved < <(ordered_profile_names_with_auth) | |
| else | |
| for spec in "$@"; do | |
| name="$(resolve_profile_spec "$spec")" | |
| require_profile_auth "$name" | |
| resolved+=("$name") | |
| done | |
| fi | |
| local -A seen=() | |
| local profile | |
| for profile in "${resolved[@]}"; do | |
| if [[ -n "${seen[$profile]:-}" ]]; then | |
| continue | |
| fi | |
| seen["$profile"]=1 | |
| printf '%s\n' "$profile" | |
| done | |
| } | |
| sync_t3_accounts_to_settings_file() { | |
| local settings_path="$1" | |
| local primary_home="$2" | |
| shift 2 | |
| local -a backup_homes=("$@") | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] update T3 settings %q with primary %q and %s backup account(s)\n' \ | |
| "$settings_path" "$primary_home" "${#backup_homes[@]}" | |
| return 0 | |
| fi | |
| mkdir -p "$(dirname "$settings_path")" | |
| if [[ ! -f "$settings_path" ]]; then | |
| printf '{}\n' > "$settings_path" | |
| fi | |
| python3 - "$settings_path" "$primary_home" "${backup_homes[@]}" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| settings_path = Path(sys.argv[1]).expanduser() | |
| primary_home = sys.argv[2] | |
| backup_homes = list(sys.argv[3:]) | |
| try: | |
| data = json.loads(settings_path.read_text()) | |
| except Exception: | |
| data = {} | |
| if not isinstance(data, dict): | |
| data = {} | |
| providers = data.get("providers") | |
| if not isinstance(providers, dict): | |
| providers = {} | |
| data["providers"] = providers | |
| codex = providers.get("codex") | |
| if not isinstance(codex, dict): | |
| codex = {} | |
| providers["codex"] = codex | |
| codex["homePath"] = primary_home | |
| codex["accountHomePaths"] = backup_homes | |
| provider_instances = data.get("providerInstances") | |
| if not isinstance(provider_instances, dict): | |
| provider_instances = {} | |
| data["providerInstances"] = provider_instances | |
| codex_instance = provider_instances.get("codex") | |
| if not isinstance(codex_instance, dict): | |
| codex_instance = {"driver": "codex"} | |
| provider_instances["codex"] = codex_instance | |
| config = codex_instance.get("config") | |
| if not isinstance(config, dict): | |
| config = {} | |
| codex_instance["config"] = config | |
| config["homePath"] = primary_home | |
| config["accountHomePaths"] = backup_homes | |
| settings_path.write_text(json.dumps(data, indent=2) + "\n") | |
| PY | |
| } | |
| sync_profiles_to_t3_settings() { | |
| local -a profiles=("$@") | |
| (( ${#profiles[@]} > 0 )) || die "no profile homes with auth.json are available to sync" | |
| local primary_profile="${profiles[0]}" | |
| local primary_home | |
| primary_home="$(profile_home_dir "$primary_profile")" | |
| local -a backup_homes=() | |
| local profile settings_path | |
| for profile in "${profiles[@]:1}"; do | |
| backup_homes+=("$(profile_home_dir "$profile")") | |
| done | |
| for settings_path in "${T3_SETTINGS_PATHS[@]}"; do | |
| log "Syncing T3 Codex accounts to $settings_path" | |
| sync_t3_accounts_to_settings_file "$settings_path" "$primary_home" "${backup_homes[@]}" | |
| done | |
| log "Primary T3 Codex account: $primary_profile -> $primary_home" | |
| if (( ${#backup_homes[@]} > 0 )); then | |
| log "Backup T3 Codex accounts:" | |
| local i | |
| for ((i = 1; i < ${#profiles[@]}; i++)); do | |
| log " ${profiles[$i]} -> ${backup_homes[$((i - 1))]}" | |
| done | |
| else | |
| log "No backup T3 Codex accounts configured." | |
| fi | |
| } | |
| auth_freshness_epoch() { | |
| local path="$1" | |
| python3 - "$path" <<'PY' | |
| import base64 | |
| import json | |
| import sys | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| path = Path(sys.argv[1]) | |
| try: | |
| data = json.loads(path.read_text()) | |
| except Exception: | |
| print("") | |
| raise SystemExit | |
| def parse_iso8601(value): | |
| if not isinstance(value, str) or not value: | |
| return None | |
| try: | |
| normalized = value.replace("Z", "+00:00") | |
| if "." in normalized: | |
| head, tail = normalized.split(".", 1) | |
| tz_index = max(tail.find("+"), tail.find("-")) | |
| if tz_index != -1: | |
| frac = tail[:tz_index] | |
| tz = tail[tz_index:] | |
| else: | |
| frac = tail | |
| tz = "" | |
| frac = (frac[:6]).ljust(6, "0") | |
| normalized = f"{head}.{frac}{tz}" | |
| return int(datetime.fromisoformat(normalized).timestamp()) | |
| except Exception: | |
| return None | |
| def decode_jwt_epoch(token): | |
| if not isinstance(token, str) or token.count(".") < 2: | |
| return None | |
| try: | |
| payload = token.split(".")[1] | |
| payload += "=" * (-len(payload) % 4) | |
| data = json.loads(base64.urlsafe_b64decode(payload)) | |
| except Exception: | |
| return None | |
| for key in ("iat", "nbf", "exp"): | |
| value = data.get(key) | |
| if isinstance(value, int): | |
| return value | |
| return None | |
| last_refresh = parse_iso8601(data.get("last_refresh")) | |
| if last_refresh is not None: | |
| print(last_refresh) | |
| raise SystemExit | |
| tokens = data.get("tokens", {}) if isinstance(data, dict) else {} | |
| if isinstance(tokens, dict): | |
| for key in ("access_token", "id_token", "refresh_token"): | |
| epoch = decode_jwt_epoch(tokens.get(key)) | |
| if epoch is not None: | |
| print(epoch) | |
| raise SystemExit | |
| print("") | |
| PY | |
| } | |
| auth_freshness_label() { | |
| local path="$1" | |
| python3 - "$path" <<'PY' | |
| import base64 | |
| import json | |
| import sys | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| path = Path(sys.argv[1]) | |
| try: | |
| data = json.loads(path.read_text()) | |
| except Exception: | |
| print("-") | |
| raise SystemExit | |
| def parse_iso8601(value): | |
| if not isinstance(value, str) or not value: | |
| return None | |
| try: | |
| normalized = value.replace("Z", "+00:00") | |
| if "." in normalized: | |
| head, tail = normalized.split(".", 1) | |
| tz_index = max(tail.find("+"), tail.find("-")) | |
| if tz_index != -1: | |
| frac = tail[:tz_index] | |
| tz = tail[tz_index:] | |
| else: | |
| frac = tail | |
| tz = "" | |
| frac = (frac[:6]).ljust(6, "0") | |
| normalized = f"{head}.{frac}{tz}" | |
| return datetime.fromisoformat(normalized) | |
| except Exception: | |
| return None | |
| def decode_jwt_payload(token): | |
| if not isinstance(token, str) or token.count(".") < 2: | |
| return None | |
| try: | |
| payload = token.split(".")[1] | |
| payload += "=" * (-len(payload) % 4) | |
| return json.loads(base64.urlsafe_b64decode(payload)) | |
| except Exception: | |
| return None | |
| last_refresh = parse_iso8601(data.get("last_refresh")) | |
| if last_refresh is not None: | |
| print(f"last_refresh={last_refresh.astimezone(timezone.utc).isoformat().replace('+00:00', 'Z')}") | |
| raise SystemExit | |
| tokens = data.get("tokens", {}) if isinstance(data, dict) else {} | |
| if isinstance(tokens, dict): | |
| for token_key in ("access_token", "id_token", "refresh_token"): | |
| payload = decode_jwt_payload(tokens.get(token_key)) | |
| if not isinstance(payload, dict): | |
| continue | |
| for claim in ("iat", "nbf", "exp"): | |
| value = payload.get(claim) | |
| if isinstance(value, int): | |
| stamp = datetime.fromtimestamp(value, tz=timezone.utc).isoformat().replace("+00:00", "Z") | |
| print(f"{token_key}.{claim}={stamp}") | |
| raise SystemExit | |
| print("mtime-only") | |
| PY | |
| } | |
| profile_auth_is_older_than_source() { | |
| local name="$1" | |
| local source="$2" | |
| local dest="$PROFILES_DIR/$name/auth.json" | |
| local source_epoch dest_epoch | |
| [[ -f "$source" ]] || return 1 | |
| [[ -f "$dest" ]] || return 0 | |
| source_epoch="$(auth_freshness_epoch "$source")" | |
| dest_epoch="$(auth_freshness_epoch "$dest")" | |
| if [[ -n "$source_epoch" && -n "$dest_epoch" ]]; then | |
| (( source_epoch > dest_epoch )) | |
| return $? | |
| fi | |
| [[ "$source" -nt "$dest" ]] | |
| } | |
| profile_auth_freshness_summary() { | |
| local name="$1" | |
| local source="$2" | |
| local dest="$PROFILES_DIR/$name/auth.json" | |
| local source_label dest_label | |
| source_label="$(auth_freshness_label "$source")" | |
| if [[ -f "$dest" ]]; then | |
| dest_label="$(auth_freshness_label "$dest")" | |
| else | |
| dest_label="missing" | |
| fi | |
| printf 'source=%s existing=%s\n' "$source_label" "$dest_label" | |
| } | |
| profile_is_registered() { | |
| local name="$1" | |
| profile_exists_in_order "$name" && return 0 | |
| [[ -d "$PROFILES_DIR/$name" ]] | |
| } | |
| cmd_discover() { | |
| local -a auth_paths=() | |
| local -a candidates=() | |
| local path index=0 | |
| : > "$DISCOVER_CACHE_FILE" | |
| candidates+=("$HOME/.codex/auth.json") | |
| shopt -s nullglob | |
| for path in "$HOME"/.codex-*/auth.json; do | |
| candidates+=("$path") | |
| done | |
| for path in /mnt/c/Users/*/.codex/auth.json; do | |
| candidates+=("$path") | |
| done | |
| for path in "$HOME/.codex-switch/profiles/"*.json; do | |
| candidates+=("$path") | |
| done | |
| for path in /mnt/c/Users/*/.codex-switch/profiles/*.json; do | |
| candidates+=("$path") | |
| done | |
| shopt -u nullglob | |
| declare -A seen=() | |
| for path in "${candidates[@]}"; do | |
| [[ -f "$path" ]] || continue | |
| [[ -n "${seen[$path]:-}" ]] && continue | |
| seen["$path"]=1 | |
| auth_paths+=("$path") | |
| done | |
| printf 'Auth candidates:\n' | |
| if (( ${#auth_paths[@]} == 0 )); then | |
| printf ' none found\n' | |
| else | |
| for path in "${auth_paths[@]}"; do | |
| index=$((index + 1)) | |
| local hint auto_imported=0 auto_import_status="" freshness_summary="" | |
| hint="$(lookup_profile_hint_for_auth "$path")" | |
| write_discover_cache "$index" "$path" "$hint" | |
| if [[ "$hint" != "-" ]] && profile_is_registered "$hint"; then | |
| freshness_summary="$(profile_auth_freshness_summary "$hint" "$path")" | |
| if profile_auth_is_older_than_source "$hint" "$path"; then | |
| import_profile_file "$hint" "$path" 0 | |
| auto_imported=1 | |
| auto_import_status="done" | |
| else | |
| auto_import_status="skipped_not_newer" | |
| fi | |
| fi | |
| if [[ "$hint" != "-" ]]; then | |
| printf ' [%s] %s (LOOKS LIKE %s)\n' "$index" "$path" "$hint" | |
| else | |
| printf ' [%s] %s\n' "$index" "$path" | |
| fi | |
| if [[ -n "$auto_import_status" ]]; then | |
| if (( auto_imported )); then | |
| printf ' auto_imported=%s\n' "$hint" | |
| else | |
| printf ' auto_import_skipped=%s\n' "$hint" | |
| fi | |
| printf ' status=%s\n' "$auto_import_status" | |
| if [[ -n "$freshness_summary" ]]; then | |
| printf ' freshness=%s\n' "$freshness_summary" | |
| fi | |
| fi | |
| printf ' size=%s\n' "$(wc -c < "$path")" | |
| printf ' label=%s\n' "$(safe_label_for_auth "$path")" | |
| done | |
| fi | |
| } | |
| cmd_inspect() { | |
| local path="${1:-}" | |
| [[ -n "$path" ]] || die "usage: codex-profiles inspect <path-to-auth.json>" | |
| [[ -f "$path" ]] || die "auth file not found: $path" | |
| show_auth_metadata "$path" | |
| } | |
| cmd_import() { | |
| local arg1="${1:-}" | |
| local arg2="${2:-}" | |
| local arg3="${3:-}" | |
| [[ -n "$arg1" ]] || die "usage: codex-profiles import <discover-number> [name] | import <name> <path-to-auth.json>" | |
| if [[ "$arg1" =~ ^[0-9]+$ ]]; then | |
| [[ -z "$arg3" ]] || die "usage: codex-profiles import <discover-number> [name]" | |
| local cache_line source hint name | |
| cache_line="$(read_discover_cache_entry "$arg1")" || die "discover entry $arg1 not found; run 'cprof discover' again" | |
| IFS=$'\t' read -r _ source hint <<< "$cache_line" | |
| if [[ -n "$arg2" ]]; then | |
| name="$arg2" | |
| elif [[ "$hint" != "-" ]]; then | |
| name="$hint" | |
| else | |
| die "discover entry $arg1 has no matched profile name; use 'cprof import $arg1 <newname>'" | |
| fi | |
| import_profile_file "$name" "$source" | |
| return 0 | |
| fi | |
| [[ -n "$arg2" && -z "$arg3" ]] || die "usage: codex-profiles import <name> <path-to-auth.json>" | |
| import_profile_file "$arg1" "$arg2" | |
| } | |
| cmd_list() { | |
| ensure_layout | |
| local found=0 auth name index=0 imported_account_id | |
| local -a headers=("ORD" "NAME" "STATUS" "IMPORTED_MTIME" "IMPORTED_ACCOUNT_ID") | |
| local -a widths mins row | |
| local -a rows=() | |
| local col_count="${#headers[@]}" | |
| local i total_width cols overflow changed | |
| for ((i = 0; i < col_count; i++)); do | |
| widths[i]="${#headers[$i]}" | |
| mins[i]="${#headers[$i]}" | |
| done | |
| while IFS= read -r name; do | |
| [[ -n "$name" ]] || continue | |
| found=1 | |
| index=$((index + 1)) | |
| auth="$PROFILES_DIR/$name/auth.json" | |
| if [[ -f "$auth" ]]; then | |
| imported_account_id="$(account_id_for_auth "$auth")" | |
| row=( | |
| "$index" | |
| "$name" | |
| "present" | |
| "$(date -r "$auth" '+%Y-%m-%d %H:%M:%S')" | |
| "$imported_account_id" | |
| ) | |
| else | |
| row=( | |
| "$index" | |
| "$name" | |
| "missing" | |
| "-" | |
| "-" | |
| ) | |
| fi | |
| rows+=("$(printf '%s\t%s\t%s\t%s\t%s' "${row[@]}")") | |
| for ((i = 0; i < col_count; i++)); do | |
| if (( ${#row[$i]} > widths[$i] )); then | |
| widths[$i]="${#row[$i]}" | |
| fi | |
| done | |
| done < <(ordered_profiles) | |
| if (( ! found )); then | |
| log "No profiles registered in $PROFILES_DIR" | |
| return 0 | |
| fi | |
| cols="$(terminal_columns)" | |
| while :; do | |
| total_width=0 | |
| for ((i = 0; i < col_count; i++)); do | |
| total_width=$((total_width + widths[i])) | |
| done | |
| total_width=$((total_width + (col_count - 1) * 2)) | |
| (( total_width <= cols )) && break | |
| overflow=$((total_width - cols)) | |
| changed=0 | |
| for i in 4 1 3 2 0; do | |
| while (( overflow > 0 && widths[i] > mins[i] )); do | |
| widths[i]=$((widths[i] - 1)) | |
| overflow=$((overflow - 1)) | |
| changed=1 | |
| done | |
| (( overflow == 0 )) && break | |
| done | |
| (( changed == 1 )) || break | |
| done | |
| print_table_row headers widths | |
| local raw_row | |
| for raw_row in "${rows[@]}"; do | |
| IFS=$'\t' read -r -a row <<< "$raw_row" | |
| print_table_row row widths | |
| done | |
| } | |
| cmd_current() { | |
| ensure_layout | |
| printf 'Live CLI state:\n' | |
| if [[ -f "$LIVE_CODEX_HOME/auth.json" ]]; then | |
| local live_account_id live_hint | |
| live_account_id="$(account_id_for_auth "$LIVE_CODEX_HOME/auth.json")" | |
| live_hint="$(lookup_profile_hint_for_account_id "$live_account_id")" | |
| if [[ "$live_hint" != "-" ]]; then | |
| printf ' logged_in_as=%s\n' "$live_hint" | |
| else | |
| printf ' logged_in_as=unknown\n' | |
| fi | |
| printf ' auth=present\n' | |
| printf ' auth_account_id=%s\n' "$live_account_id" | |
| printf ' auth_mtime=%s\n' "$(date -r "$LIVE_CODEX_HOME/auth.json" '+%Y-%m-%d %H:%M:%S')" | |
| else | |
| printf ' logged_in_as=none\n' | |
| log " auth=missing" | |
| fi | |
| if [[ -f "$PID_FILE" ]]; then | |
| printf ' managed_pid=%s\n' "$(cat "$PID_FILE")" | |
| fi | |
| } | |
| activate_profile() { | |
| local spec="$1" | |
| local name | |
| name="$(resolve_profile_spec "$spec")" | |
| require_profile_auth "$name" | |
| ensure_layout | |
| stop_related_processes | |
| backup_live_auth | |
| local source target | |
| source="$(profile_auth_path "$name")" | |
| target="$LIVE_CODEX_HOME/auth.json" | |
| log "Activating profile: $name" | |
| run_cmd cp -p "$source" "$target" | |
| run_cmd chmod 600 "$target" | |
| log "Active profile: $name" | |
| } | |
| cmd_use() { | |
| local spec="${1:-}" | |
| [[ -n "$spec" ]] || die "usage: codex-profiles use <name-or-ordinal>" | |
| activate_profile "$spec" | |
| } | |
| ensure_repo_exists() { | |
| [[ -d "$REPO_DIR" ]] || die "repo directory does not exist: $REPO_DIR" | |
| } | |
| ensure_local_t3code_repo_exists() { | |
| [[ -d "$LOCAL_T3CODE_REPO/apps/server" ]] || die "local T3 Code repo not found at $LOCAL_T3CODE_REPO" | |
| command -v bun >/dev/null 2>&1 || die "bun is required for --local-t3code" | |
| } | |
| assert_local_t3code_dev_mode() { | |
| if (( USE_LOCAL_T3CODE && USE_LOCAL_T3CODE_DEV )); then | |
| die "use only one of --local-t3code or --local-t3code-dev" | |
| fi | |
| } | |
| local_t3code_dev_server_url() { | |
| printf 'http://localhost:%s\n' "$LOCAL_T3CODE_DEV_SERVER_PORT" | |
| } | |
| local_t3code_dev_web_url() { | |
| printf 'http://localhost:%s\n' "$LOCAL_T3CODE_DEV_WEB_PORT" | |
| } | |
| start_local_t3code_dev_web_detached() { | |
| local live_real="$1" | |
| local repo_root="$LOCAL_T3CODE_REPO" | |
| local web_url | |
| web_url="$(local_t3code_dev_web_url)" | |
| log "Starting detached local T3 Code web dev server at $web_url" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && nohup env CODEX_HOME=%q VITE_HTTP_URL=%q VITE_WS_URL=%q VITE_DEV_SERVER_URL=%q bun run --cwd apps/web dev -- --host 127.0.0.1 --port %q --strictPort >> %q 2>&1 & echo $! > %q)\n' \ | |
| "$repo_root" "$live_real" "$(local_t3code_dev_server_url)" "ws://localhost:$LOCAL_T3CODE_DEV_SERVER_PORT" "$(local_t3code_dev_web_url)" "$LOCAL_T3CODE_DEV_WEB_PORT" "$DEV_WEB_LOG_FILE" "$DEV_WEB_PID_FILE" | |
| return 0 | |
| fi | |
| ( | |
| cd "$repo_root" | |
| nohup env \ | |
| CODEX_HOME="$live_real" \ | |
| VITE_HTTP_URL="$(local_t3code_dev_server_url)" \ | |
| VITE_WS_URL="ws://localhost:$LOCAL_T3CODE_DEV_SERVER_PORT" \ | |
| VITE_DEV_SERVER_URL="$(local_t3code_dev_web_url)" \ | |
| bun run --cwd apps/web dev -- --host 127.0.0.1 --port "$LOCAL_T3CODE_DEV_WEB_PORT" --strictPort \ | |
| >> "$DEV_WEB_LOG_FILE" 2>&1 & | |
| echo $! > "$DEV_WEB_PID_FILE" | |
| ) | |
| } | |
| start_local_t3code_dev_server_detached() { | |
| local repo_real="$1" | |
| local live_real="$2" | |
| local repo_root="$LOCAL_T3CODE_REPO" | |
| local web_url | |
| web_url="$(local_t3code_dev_web_url)" | |
| log "Starting detached local T3 Code server dev process for $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && nohup env CODEX_HOME=%q bun run --cwd apps/server dev -- %q --port %q --dev-url %q --no-browser --auto-bootstrap-project-from-cwd >> %q 2>&1 & echo $! > %q)\n' \ | |
| "$repo_root" "$live_real" "$repo_real" "$LOCAL_T3CODE_DEV_SERVER_PORT" "$web_url" "$DEV_SERVER_LOG_FILE" "$DEV_SERVER_PID_FILE" | |
| return 0 | |
| fi | |
| ( | |
| cd "$repo_root" | |
| nohup env \ | |
| CODEX_HOME="$live_real" \ | |
| bun run --cwd apps/server dev -- "$repo_real" --port "$LOCAL_T3CODE_DEV_SERVER_PORT" --dev-url "$web_url" --no-browser --auto-bootstrap-project-from-cwd \ | |
| >> "$DEV_SERVER_LOG_FILE" 2>&1 & | |
| echo $! > "$DEV_SERVER_PID_FILE" | |
| ) | |
| } | |
| serve_local_t3code_dev() { | |
| local repo_real="$1" | |
| local live_real="$2" | |
| local repo_root="$LOCAL_T3CODE_REPO" | |
| local web_pid="" | |
| local web_url | |
| web_url="$(local_t3code_dev_web_url)" | |
| cleanup_local_t3code_dev_web() { | |
| if [[ -n "$web_pid" ]] && kill -0 "$web_pid" 2>/dev/null; then | |
| kill "$web_pid" 2>/dev/null || true | |
| wait "$web_pid" 2>/dev/null || true | |
| fi | |
| } | |
| trap cleanup_local_t3code_dev_web EXIT INT TERM | |
| log "Starting foreground local T3 Code dev mode for $repo_real" | |
| log "Web dev URL: $web_url" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && env CODEX_HOME=%q VITE_HTTP_URL=%q VITE_WS_URL=%q VITE_DEV_SERVER_URL=%q bun run --cwd apps/web dev -- --host 127.0.0.1 --port %q --strictPort &)\n' \ | |
| "$repo_root" "$live_real" "$(local_t3code_dev_server_url)" "ws://localhost:$LOCAL_T3CODE_DEV_SERVER_PORT" "$(local_t3code_dev_web_url)" "$LOCAL_T3CODE_DEV_WEB_PORT" | |
| printf '[dry-run] (cd %q && exec env CODEX_HOME=%q bun run --cwd apps/server dev -- %q --port %q --dev-url %q --no-browser --auto-bootstrap-project-from-cwd)\n' \ | |
| "$repo_root" "$live_real" "$repo_real" "$LOCAL_T3CODE_DEV_SERVER_PORT" "$web_url" | |
| return 0 | |
| fi | |
| ( | |
| cd "$repo_root" | |
| env \ | |
| CODEX_HOME="$live_real" \ | |
| VITE_HTTP_URL="$(local_t3code_dev_server_url)" \ | |
| VITE_WS_URL="ws://localhost:$LOCAL_T3CODE_DEV_SERVER_PORT" \ | |
| VITE_DEV_SERVER_URL="$(local_t3code_dev_web_url)" \ | |
| bun run --cwd apps/web dev -- --host 127.0.0.1 --port "$LOCAL_T3CODE_DEV_WEB_PORT" --strictPort | |
| ) & | |
| web_pid=$! | |
| cd "$repo_root" | |
| exec env \ | |
| CODEX_HOME="$live_real" \ | |
| bun run --cwd apps/server dev -- "$repo_real" --port "$LOCAL_T3CODE_DEV_SERVER_PORT" --dev-url "$web_url" --no-browser --auto-bootstrap-project-from-cwd | |
| } | |
| build_local_t3code() { | |
| ensure_local_t3code_repo_exists | |
| local repo_root="$LOCAL_T3CODE_REPO" | |
| if ! local_t3code_needs_build "$repo_root"; then | |
| log "Local T3 Code build is up to date at $repo_root" | |
| return 0 | |
| fi | |
| log "Building local T3 Code web client and server bundle from $repo_root" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && bun run --cwd apps/web build && bun run --cwd apps/server build)\n' \ | |
| "$repo_root" | |
| return 0 | |
| fi | |
| ( | |
| cd "$repo_root" | |
| bun run --cwd apps/web build | |
| bun run --cwd apps/server build | |
| ) | |
| } | |
| local_t3code_needs_build() { | |
| local repo_root="$1" | |
| python3 - "$repo_root" <<'PY' | |
| import os | |
| import sys | |
| from pathlib import Path | |
| repo = Path(sys.argv[1]).expanduser() | |
| required_outputs = [ | |
| repo / "apps/server/dist/bin.mjs", | |
| repo / "apps/server/dist/client/index.html", | |
| ] | |
| missing = [str(path) for path in required_outputs if not path.exists()] | |
| if missing: | |
| print(f"missing build output: {missing[0]}", file=sys.stderr) | |
| raise SystemExit(0) | |
| oldest_output_mtime = min(path.stat().st_mtime for path in required_outputs) | |
| watch_files = [ | |
| repo / "package.json", | |
| repo / "bun.lock", | |
| repo / "apps/server/package.json", | |
| repo / "apps/server/tsdown.config.ts", | |
| repo / "apps/web/package.json", | |
| repo / "apps/web/vite.config.ts", | |
| ] | |
| watch_dirs = [ | |
| repo / "apps/server/src", | |
| repo / "apps/server/scripts", | |
| repo / "apps/web/src", | |
| repo / "apps/web/public", | |
| ] | |
| latest_source_mtime = 0.0 | |
| latest_source_path = None | |
| for path in watch_files: | |
| if path.exists(): | |
| mtime = path.stat().st_mtime | |
| if mtime > latest_source_mtime: | |
| latest_source_mtime = mtime | |
| latest_source_path = path | |
| for root in watch_dirs: | |
| if not root.exists(): | |
| continue | |
| for dirpath, _, filenames in os.walk(root): | |
| for filename in filenames: | |
| path = Path(dirpath) / filename | |
| try: | |
| mtime = path.stat().st_mtime | |
| except FileNotFoundError: | |
| continue | |
| if mtime > latest_source_mtime: | |
| latest_source_mtime = mtime | |
| latest_source_path = path | |
| if latest_source_mtime > oldest_output_mtime: | |
| print(f"source newer than build output: {latest_source_path}", file=sys.stderr) | |
| raise SystemExit(0) | |
| raise SystemExit(1) | |
| PY | |
| } | |
| launch_t3_detached() { | |
| ensure_repo_exists | |
| ensure_layout | |
| local repo_real live_real server_dir | |
| repo_real="$(repo_realpath)" | |
| live_real="$(live_realpath)" | |
| if (( USE_LOCAL_T3CODE_DEV )); then | |
| start_local_t3code_dev_web_detached "$live_real" | |
| start_local_t3code_dev_server_detached "$repo_real" "$live_real" | |
| elif (( USE_LOCAL_T3CODE )); then | |
| build_local_t3code | |
| server_dir="$LOCAL_T3CODE_REPO/apps/server" | |
| log "Starting detached local T3 Code from $server_dir for project $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && nohup env CODEX_HOME=%q node dist/bin.mjs %q >> %q 2>&1 & echo $! > %q)\n' \ | |
| "$server_dir" "$live_real" "$repo_real" "$LOG_FILE" "$PID_FILE" | |
| return 0 | |
| fi | |
| ( | |
| cd "$server_dir" | |
| nohup env CODEX_HOME="$live_real" node dist/bin.mjs "$repo_real" >> "$LOG_FILE" 2>&1 & | |
| echo $! > "$PID_FILE" | |
| ) | |
| else | |
| log "Starting detached T3 in $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && nohup env CODEX_HOME=%q %q %q >> %q 2>&1 & echo $! > %q)\n' \ | |
| "$repo_real" "$live_real" "${T3_COMMAND[0]}" "${T3_COMMAND[1]}" "$LOG_FILE" "$PID_FILE" | |
| return 0 | |
| fi | |
| ( | |
| cd "$repo_real" | |
| nohup env CODEX_HOME="$live_real" "${T3_COMMAND[@]}" >> "$LOG_FILE" 2>&1 & | |
| echo $! > "$PID_FILE" | |
| ) | |
| fi | |
| if (( DRY_RUN )); then | |
| return 0 | |
| fi | |
| sleep 1 | |
| if (( USE_LOCAL_T3CODE_DEV )); then | |
| if [[ -f "$DEV_SERVER_PID_FILE" ]] && [[ -f "$DEV_WEB_PID_FILE" ]] && \ | |
| kill -0 "$(cat "$DEV_SERVER_PID_FILE")" 2>/dev/null && \ | |
| kill -0 "$(cat "$DEV_WEB_PID_FILE")" 2>/dev/null; then | |
| log "Detached T3 Code dev server started with pid $(cat "$DEV_SERVER_PID_FILE")" | |
| log "Detached T3 Code web dev server started with pid $(cat "$DEV_WEB_PID_FILE")" | |
| log "Server log file: $DEV_SERVER_LOG_FILE" | |
| log "Web log file: $DEV_WEB_LOG_FILE" | |
| return 0 | |
| fi | |
| die "detached local T3 Code dev start did not leave both processes running; inspect $DEV_SERVER_LOG_FILE and $DEV_WEB_LOG_FILE" | |
| elif [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then | |
| log "Detached T3 started with pid $(cat "$PID_FILE")" | |
| log "Log file: $LOG_FILE" | |
| else | |
| die "detached start did not leave a running pid; inspect $LOG_FILE" | |
| fi | |
| } | |
| cmd_start() { | |
| local spec="${1:-}" | |
| [[ -n "$spec" ]] || die "usage: codex-profiles start <name-or-ordinal>" | |
| activate_profile "$spec" | |
| launch_t3_detached | |
| } | |
| cmd_serve() { | |
| local spec="${1:-}" | |
| [[ -n "$spec" ]] || die "usage: codex-profiles serve <name-or-ordinal>" | |
| activate_profile "$spec" | |
| ensure_repo_exists | |
| local repo_real live_real server_dir | |
| repo_real="$(repo_realpath)" | |
| live_real="$(live_realpath)" | |
| if (( USE_LOCAL_T3CODE_DEV )); then | |
| serve_local_t3code_dev "$repo_real" "$live_real" | |
| elif (( USE_LOCAL_T3CODE )); then | |
| build_local_t3code | |
| server_dir="$LOCAL_T3CODE_REPO/apps/server" | |
| log "Launching foreground local T3 Code from $server_dir for project $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && exec env CODEX_HOME=%q node dist/bin.mjs %q)\n' \ | |
| "$server_dir" "$live_real" "$repo_real" | |
| return 0 | |
| fi | |
| cd "$server_dir" | |
| exec env CODEX_HOME="$live_real" node dist/bin.mjs "$repo_real" | |
| fi | |
| log "Launching foreground T3 in $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && exec env CODEX_HOME=%q %q %q)\n' \ | |
| "$repo_real" "$live_real" "${T3_COMMAND[0]}" "${T3_COMMAND[1]}" | |
| return 0 | |
| fi | |
| cd "$repo_real" | |
| exec env CODEX_HOME="$live_real" "${T3_COMMAND[@]}" | |
| } | |
| cmd_t3_start() { | |
| ensure_layout | |
| stop_related_processes | |
| launch_t3_detached | |
| } | |
| cmd_t3_serve() { | |
| ensure_layout | |
| stop_related_processes | |
| ensure_repo_exists | |
| local repo_real live_real server_dir | |
| repo_real="$(repo_realpath)" | |
| live_real="$(live_realpath)" | |
| if (( USE_LOCAL_T3CODE_DEV )); then | |
| serve_local_t3code_dev "$repo_real" "$live_real" | |
| elif (( USE_LOCAL_T3CODE )); then | |
| build_local_t3code | |
| server_dir="$LOCAL_T3CODE_REPO/apps/server" | |
| log "Launching foreground local T3 Code from $server_dir for project $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && exec env CODEX_HOME=%q node dist/bin.mjs %q)\n' \ | |
| "$server_dir" "$live_real" "$repo_real" | |
| return 0 | |
| fi | |
| cd "$server_dir" | |
| exec env CODEX_HOME="$live_real" node dist/bin.mjs "$repo_real" | |
| fi | |
| log "Launching foreground T3 in $repo_real" | |
| if (( DRY_RUN )); then | |
| printf '[dry-run] (cd %q && exec env CODEX_HOME=%q %q %q)\n' \ | |
| "$repo_real" "$live_real" "${T3_COMMAND[0]}" "${T3_COMMAND[1]}" | |
| return 0 | |
| fi | |
| cd "$repo_real" | |
| exec env CODEX_HOME="$live_real" "${T3_COMMAND[@]}" | |
| } | |
| cmd_sync_t3_accounts() { | |
| ensure_layout | |
| local -a profiles=() | |
| mapfile -t profiles < <(resolve_profiles_for_t3_sync "$@") | |
| sync_profiles_to_t3_settings "${profiles[@]}" | |
| } | |
| cmd_import_t3_accounts() { | |
| cmd_sync_t3_accounts "$@" | |
| } | |
| cmd_stop() { | |
| ensure_layout | |
| stop_related_processes | |
| } | |
| main() { | |
| local -a positional=() | |
| while (($# > 0)); do | |
| case "${1:-}" in | |
| --dry-run) | |
| DRY_RUN=1 | |
| ;; | |
| --local-t3code) | |
| USE_LOCAL_T3CODE=1 | |
| ;; | |
| --local-t3code-dev) | |
| USE_LOCAL_T3CODE_DEV=1 | |
| ;; | |
| --help|-h) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| positional+=("$1") | |
| ;; | |
| esac | |
| shift | |
| done | |
| assert_local_t3code_dev_mode | |
| local command="${positional[0]:-help}" | |
| if ((${#positional[@]} > 0)); then | |
| positional=("${positional[@]:1}") | |
| fi | |
| case "$command" in | |
| discover) cmd_discover "${positional[@]}" ;; | |
| inspect) cmd_inspect "${positional[@]}" ;; | |
| import) cmd_import "${positional[@]}" ;; | |
| list) cmd_list "${positional[@]}" ;; | |
| current) cmd_current "${positional[@]}" ;; | |
| use) cmd_use "${positional[@]}" ;; | |
| start) cmd_start "${positional[@]}" ;; | |
| serve) cmd_serve "${positional[@]}" ;; | |
| t3-start) cmd_t3_start "${positional[@]}" ;; | |
| t3-serve) cmd_t3_serve "${positional[@]}" ;; | |
| sync-t3-accounts) cmd_sync_t3_accounts "${positional[@]}" ;; | |
| import-t3-accounts) cmd_import_t3_accounts "${positional[@]}" ;; | |
| stop) cmd_stop "${positional[@]}" ;; | |
| help|-h|--help) usage ;; | |
| *) die "unknown command: $command" ;; | |
| esac | |
| } | |
| main "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| _cprof_profiles() { | |
| local profiles_dir order_file line | |
| profiles_dir="${CODEX_PROFILES_DIR:-$HOME/.codex-profiles}" | |
| order_file="$profiles_dir/.order" | |
| if [[ -f "$order_file" ]]; then | |
| while IFS= read -r line; do | |
| [[ -n "$line" ]] || continue | |
| printf '%s\n' "$line" | |
| done < "$order_file" | |
| fi | |
| } | |
| _cprof_ordinals() { | |
| local count=0 line | |
| while IFS= read -r line; do | |
| [[ -n "$line" ]] || continue | |
| count=$((count + 1)) | |
| printf '%s\n' "$count" | |
| done < <(_cprof_profiles) | |
| } | |
| _cprof_complete() { | |
| local cur prev command | |
| COMPREPLY=() | |
| cur="${COMP_WORDS[COMP_CWORD]}" | |
| prev="${COMP_WORDS[COMP_CWORD-1]}" | |
| command="${COMP_WORDS[1]:-}" | |
| if (( COMP_CWORD == 1 )); then | |
| COMPREPLY=($(compgen -W "discover import list current use start serve t3-start t3-serve sync-t3-accounts import-t3-accounts stop help" -- "$cur")) | |
| return 0 | |
| fi | |
| case "$command" in | |
| use|start|serve|sync-t3-accounts|import-t3-accounts) | |
| COMPREPLY=($(compgen -W "$(_cprof_profiles; _cprof_ordinals)" -- "$cur")) | |
| return 0 | |
| ;; | |
| t3-start|t3-serve|discover|list|current|stop|help) | |
| return 0 | |
| ;; | |
| import) | |
| if (( COMP_CWORD == 2 )); then | |
| COMPREPLY=($(compgen -W "$(_cprof_profiles)" -- "$cur")) | |
| else | |
| compopt -o filenames 2>/dev/null | |
| COMPREPLY=($(compgen -f -- "$cur")) | |
| fi | |
| return 0 | |
| ;; | |
| esac | |
| } | |
| complete -F _cprof_complete cprof | |
| complete -F _cprof_complete codex-profiles |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| umask 077 | |
| CONFIG_FILE="${T3_CHECKPOINTS_CONFIG_FILE:-$HOME/.t3-checkpoints/config.env}" | |
| if [[ ! -f "$CONFIG_FILE" ]]; then | |
| mkdir -p "$(dirname "$CONFIG_FILE")" | |
| cat > "$CONFIG_FILE" <<'EOF' | |
| # t3-checkpoints local config | |
| # | |
| # Uncomment and edit any values you want to override for this machine. | |
| # | |
| # T3_CHECKPOINTS_REPO_DIR="$HOME/src/FireMUD" | |
| # T3_CHECKPOINTS_CODEX_ROOT="$HOME/.codex" | |
| # T3_CHECKPOINTS_T3CODE_DB_PATHS="$HOME/.t3/userdata/state.sqlite:$HOME/.t3/dev/state.sqlite" | |
| # T3_CHECKPOINTS_DEFAULT_KEEP="5" | |
| # T3_CHECKPOINTS_DEFAULT_PRUNE_DAYS="5" | |
| EOF | |
| chmod 600 "$CONFIG_FILE" | |
| fi | |
| # shellcheck disable=SC1090 | |
| source "$CONFIG_FILE" | |
| DEFAULT_KEEP="${T3_CHECKPOINTS_DEFAULT_KEEP:-5}" | |
| DEFAULT_PRUNE_DAYS="${T3_CHECKPOINTS_DEFAULT_PRUNE_DAYS:-5}" | |
| MIN_SAFE_KEEP_COMMITS=3 | |
| KEEP="$DEFAULT_KEEP" | |
| PRUNE_DAYS="$DEFAULT_PRUNE_DAYS" | |
| APPLY=0 | |
| RUN_GC=1 | |
| INSPECT_CODEX=1 | |
| INSPECT_T3CODE=1 | |
| CODEX_ROOT="${T3_CHECKPOINTS_CODEX_ROOT:-$HOME/.codex}" | |
| DEFAULT_T3CODE_DB_PATHS="$HOME/.t3/userdata/state.sqlite:$HOME/.t3/dev/state.sqlite" | |
| T3CODE_DB_PATHS_RAW="${T3_CHECKPOINTS_T3CODE_DB_PATHS:-$DEFAULT_T3CODE_DB_PATHS}" | |
| declare -a T3CODE_DB_PATHS=() | |
| REPO_DIR="${T3_CHECKPOINTS_REPO_DIR:-}" | |
| COMMAND="" | |
| MODE="" | |
| usage() { | |
| cat <<'EOF' | |
| Inspect and optionally prune T3 Code checkpoint refs. | |
| Usage: | |
| t3-checkpoints help | |
| t3-checkpoints plan prune-chat-checkpoints [N] [options] | |
| t3-checkpoints apply prune-chat-checkpoints [N] [options] | |
| t3-checkpoints plan clear-chat-checkpoints [N] [options] | |
| t3-checkpoints apply clear-chat-checkpoints [N] [options] | |
| t3cp ... | |
| Commands: | |
| help | |
| Show this help text. | |
| plan | |
| Report what would be deleted. | |
| apply | |
| Delete refs after reporting the plan. | |
| Modes: | |
| prune-chat-checkpoints [N] | |
| Keep turn 0 plus the latest N-1 non-baseline checkpoint refs per thread. | |
| This preserves the canonical baseline ref used by T3 Code diffs. Trim mode | |
| enforces a minimum of 3 total refs so the previous-turn and latest-turn | |
| diff flow keeps working. Default: 5. | |
| clear-chat-checkpoints [N] | |
| Delete every checkpoint ref for threads whose latest checkpoint is older | |
| than N days. This removes the entire checkpoint history for those chats. | |
| After prune, T3/Codex-style checkpoint diff viewing and checkpoint-based | |
| revert for those chats will no longer work. Default: 5. | |
| Options for plan/apply: | |
| --repo PATH | |
| Operate on the Git repo at PATH. | |
| --no-gc | |
| Skip reflog expiry and git gc after deleting refs. Applies to `apply`. | |
| --codex-root PATH | |
| Inspect Codex state under PATH instead of ~/.codex. | |
| --no-codex-inspect | |
| Skip local Codex-state inspection and classification. Advisory only. | |
| --t3-db PATH | |
| Inspect a specific T3 Code SQLite state database. May be passed multiple | |
| times. Defaults to ~/.t3/userdata/state.sqlite and ~/.t3/dev/state.sqlite. | |
| --no-t3code-inspect | |
| Skip T3 Code state inspection and safety guards. Unsafe for apply. | |
| --help | |
| Show command help. | |
| This utility only considers refs matching: | |
| refs/t3/checkpoints/<thread-id>/turn/<integer> | |
| It never touches normal branches, tags, remotes, or other refs. | |
| When enabled, T3 Code state inspection is treated as authoritative for whether | |
| checkpoint refs are still needed for live or archived threads. | |
| EOF | |
| } | |
| die() { | |
| echo "Error: $*" >&2 | |
| exit 1 | |
| } | |
| validate_positive_integer() { | |
| local value="$1" | |
| [[ "$value" =~ ^[1-9][0-9]*$ ]] | |
| } | |
| format_epoch() { | |
| local epoch="$1" | |
| if [[ -z "$epoch" || "$epoch" == "0" ]]; then | |
| printf 'unknown' | |
| return | |
| fi | |
| date -d "@$epoch" '+%Y-%m-%d %H:%M:%S %z' | |
| } | |
| inspect_codex_state() { | |
| local thread_file="$1" | |
| local output_file="$2" | |
| if ! command -v python3 >/dev/null 2>&1; then | |
| echo "Codex-state inspection unavailable: python3 not found." >&2 | |
| return 1 | |
| fi | |
| if ! python3 - "$CODEX_ROOT" "$thread_file" >"$output_file" <<'PY' | |
| import base64 | |
| import json | |
| import sqlite3 | |
| import sys | |
| from pathlib import Path | |
| codex_root = Path(sys.argv[1]).expanduser() | |
| thread_file = Path(sys.argv[2]) | |
| thread_ids = [line.strip() for line in thread_file.read_text().splitlines() if line.strip()] | |
| def decode_thread_id(value: str): | |
| padded = value + "=" * ((4 - len(value) % 4) % 4) | |
| try: | |
| decoded = base64.urlsafe_b64decode(padded.encode("ascii")) | |
| except Exception: | |
| return None | |
| if len(decoded) == 16: | |
| try: | |
| import uuid | |
| return str(uuid.UUID(bytes=decoded)) | |
| except Exception: | |
| return None | |
| try: | |
| text = decoded.decode("utf-8") | |
| except UnicodeDecodeError: | |
| return None | |
| return text if text and text.isprintable() else None | |
| records = [] | |
| db_thread_ids = set() | |
| db_path = codex_root / "state_5.sqlite" | |
| db_status = "missing" | |
| db_error = None | |
| if db_path.exists(): | |
| try: | |
| with sqlite3.connect(db_path) as conn: | |
| tables = {row[0] for row in conn.execute("select name from sqlite_master where type = 'table'")} | |
| if "threads" in tables: | |
| columns = {row[1] for row in conn.execute("pragma table_info(threads)")} | |
| if "id" in columns: | |
| db_thread_ids = { | |
| str(row[0]) for row in conn.execute("select id from threads") if row[0] is not None | |
| } | |
| db_status = "threads.id" | |
| else: | |
| db_status = "threads table without id column" | |
| else: | |
| db_status = "threads table missing" | |
| except Exception as exc: | |
| db_status = "error" | |
| db_error = str(exc) | |
| sessions_root = codex_root / "sessions" | |
| session_hits = {thread_id: [] for thread_id in thread_ids} | |
| session_status = "missing" | |
| if sessions_root.exists(): | |
| session_status = "scanned" | |
| search_tokens = {} | |
| for thread_id in thread_ids: | |
| tokens = {thread_id} | |
| decoded = decode_thread_id(thread_id) | |
| if decoded: | |
| tokens.add(decoded) | |
| search_tokens[thread_id] = tuple(token for token in tokens if token) | |
| for path in sessions_root.rglob("*"): | |
| if not path.is_file() or path.suffix not in {".json", ".jsonl"}: | |
| continue | |
| try: | |
| text = path.read_text(errors="ignore") | |
| except Exception: | |
| continue | |
| rel = str(path.relative_to(codex_root)) | |
| for thread_id, tokens in search_tokens.items(): | |
| if session_hits[thread_id]: | |
| continue | |
| if any(token in text for token in tokens): | |
| session_hits[thread_id].append(rel) | |
| for thread_id in thread_ids: | |
| decoded = decode_thread_id(thread_id) | |
| exact_db = thread_id in db_thread_ids | |
| decoded_db = bool(decoded and decoded in db_thread_ids) | |
| session_paths = session_hits.get(thread_id, []) | |
| if exact_db or decoded_db: | |
| classification = "active-candidate" | |
| elif session_paths: | |
| classification = "archived-candidate" | |
| else: | |
| classification = "orphaned-candidate" | |
| details = [] | |
| if exact_db: | |
| details.append("exact sqlite thread id match") | |
| if decoded_db: | |
| details.append(f"decoded sqlite thread id match ({decoded})") | |
| elif decoded: | |
| details.append(f"decoded id candidate: {decoded}") | |
| if session_paths: | |
| details.append(f"session metadata hit: {session_paths[0]}") | |
| if not details: | |
| details.append("no sqlite or session metadata match found") | |
| records.append( | |
| { | |
| "threadId": thread_id, | |
| "classification": classification, | |
| "details": "; ".join(details), | |
| } | |
| ) | |
| result = { | |
| "codexRoot": str(codex_root), | |
| "dbStatus": db_status, | |
| "dbError": db_error, | |
| "sessionStatus": session_status, | |
| "records": records, | |
| } | |
| print(json.dumps(result)) | |
| PY | |
| then | |
| echo "Codex-state inspection failed." >&2 | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| populate_t3code_db_paths() { | |
| local raw_path path | |
| local old_ifs="$IFS" | |
| IFS=':' | |
| read -r -a raw_paths <<<"$T3CODE_DB_PATHS_RAW" | |
| IFS="$old_ifs" | |
| for raw_path in "${raw_paths[@]}"; do | |
| path="${raw_path/#\~/$HOME}" | |
| [[ -n "$path" ]] || continue | |
| T3CODE_DB_PATHS+=("$path") | |
| done | |
| } | |
| inspect_t3code_state() { | |
| local thread_file="$1" | |
| local output_file="$2" | |
| if ! command -v python3 >/dev/null 2>&1; then | |
| echo "T3 Code state inspection unavailable: python3 not found." >&2 | |
| return 1 | |
| fi | |
| local joined_paths | |
| joined_paths="$(printf '%s\n' "${T3CODE_DB_PATHS[@]}")" | |
| if ! T3CP_T3CODE_DB_PATHS="$joined_paths" python3 - "$thread_file" "$output_file" <<'PY' | |
| import json | |
| import os | |
| import sqlite3 | |
| import sys | |
| from pathlib import Path | |
| thread_file = Path(sys.argv[1]) | |
| output_file = Path(sys.argv[2]) | |
| db_paths = [line.strip() for line in os.environ.get("T3CP_T3CODE_DB_PATHS", "").splitlines() if line.strip()] | |
| thread_ids = [line.strip() for line in thread_file.read_text().splitlines() if line.strip()] | |
| records = {} | |
| db_reports = [] | |
| for thread_id in thread_ids: | |
| records[thread_id] = { | |
| "threadId": thread_id, | |
| "classification": "orphaned-candidate", | |
| "details": [], | |
| "sources": [], | |
| } | |
| def add_detail(thread_id: str, db_path: Path, detail: str): | |
| record = records[thread_id] | |
| record["details"].append(detail) | |
| record["sources"].append(str(db_path)) | |
| def ensure_columns(conn, table): | |
| return {row[1] for row in conn.execute(f"pragma table_info({table})")} | |
| for raw_db_path in db_paths: | |
| db_path = Path(raw_db_path).expanduser() | |
| report = { | |
| "path": str(db_path), | |
| "status": "missing", | |
| "error": None, | |
| } | |
| if not db_path.exists(): | |
| db_reports.append(report) | |
| continue | |
| try: | |
| with sqlite3.connect(db_path) as conn: | |
| tables = {row[0] for row in conn.execute("select name from sqlite_master where type = 'table'")} | |
| report["status"] = "opened" | |
| thread_columns = ensure_columns(conn, "projection_threads") if "projection_threads" in tables else set() | |
| runtime_columns = ensure_columns(conn, "provider_session_runtime") if "provider_session_runtime" in tables else set() | |
| turn_columns = ensure_columns(conn, "projection_turns") if "projection_turns" in tables else set() | |
| thread_rows = {} | |
| if "projection_threads" in tables and "thread_id" in thread_columns: | |
| for row in conn.execute( | |
| """ | |
| select thread_id, archived_at, deleted_at | |
| from projection_threads | |
| where thread_id in (%s) | |
| """ | |
| % ",".join("?" for _ in thread_ids), | |
| thread_ids, | |
| ): | |
| thread_rows[str(row[0])] = { | |
| "archived_at": row[1], | |
| "deleted_at": row[2], | |
| } | |
| runtime_rows = set() | |
| if "provider_session_runtime" in tables and "thread_id" in runtime_columns: | |
| runtime_rows = { | |
| str(row[0]) | |
| for row in conn.execute( | |
| """ | |
| select thread_id | |
| from provider_session_runtime | |
| where thread_id in (%s) | |
| """ | |
| % ",".join("?" for _ in thread_ids), | |
| thread_ids, | |
| ) | |
| } | |
| checkpoint_counts = {} | |
| if ( | |
| "projection_turns" in tables | |
| and "thread_id" in turn_columns | |
| and "checkpoint_turn_count" in turn_columns | |
| ): | |
| for row in conn.execute( | |
| """ | |
| select thread_id, count(*) | |
| from projection_turns | |
| where thread_id in (%s) | |
| and checkpoint_turn_count is not null | |
| group by thread_id | |
| """ | |
| % ",".join("?" for _ in thread_ids), | |
| thread_ids, | |
| ): | |
| checkpoint_counts[str(row[0])] = int(row[1] or 0) | |
| for thread_id in thread_ids: | |
| thread_row = thread_rows.get(thread_id) | |
| runtime_bound = thread_id in runtime_rows | |
| checkpoint_count = checkpoint_counts.get(thread_id, 0) | |
| if thread_row: | |
| deleted_at = thread_row["deleted_at"] | |
| archived_at = thread_row["archived_at"] | |
| if deleted_at: | |
| add_detail(thread_id, db_path, f"thread deleted in T3 Code state ({deleted_at})") | |
| if records[thread_id]["classification"] == "orphaned-candidate": | |
| records[thread_id]["classification"] = "deleted-candidate" | |
| elif archived_at: | |
| add_detail(thread_id, db_path, f"thread archived in T3 Code state ({archived_at})") | |
| records[thread_id]["classification"] = "archived-candidate" | |
| else: | |
| add_detail(thread_id, db_path, "thread exists in live T3 Code state") | |
| records[thread_id]["classification"] = "live-candidate" | |
| elif runtime_bound: | |
| add_detail(thread_id, db_path, "provider runtime binding still exists in T3 Code state") | |
| if records[thread_id]["classification"] not in ("live-candidate", "archived-candidate"): | |
| records[thread_id]["classification"] = "runtime-bound-candidate" | |
| elif checkpoint_count > 0: | |
| add_detail(thread_id, db_path, f"T3 Code still stores {checkpoint_count} projected checkpoint row(s)") | |
| if records[thread_id]["classification"] == "orphaned-candidate": | |
| records[thread_id]["classification"] = "lingering-state-candidate" | |
| except Exception as exc: | |
| report["status"] = "error" | |
| report["error"] = str(exc) | |
| db_reports.append(report) | |
| for record in records.values(): | |
| if not record["details"]: | |
| record["details"].append("no T3 Code state match found") | |
| record["details"] = "; ".join(record["details"]) | |
| record["sources"] = sorted(set(record["sources"])) | |
| output_file.write_text( | |
| json.dumps( | |
| { | |
| "dbReports": db_reports, | |
| "existingDbCount": sum(1 for report in db_reports if report["status"] != "missing"), | |
| "openedDbCount": sum(1 for report in db_reports if report["status"] == "opened"), | |
| "allExistingDbPathsOpened": all( | |
| report["status"] == "opened" | |
| for report in db_reports | |
| if report["status"] != "missing" | |
| ), | |
| "records": [records[thread_id] for thread_id in thread_ids], | |
| } | |
| ) | |
| ) | |
| PY | |
| then | |
| echo "T3 Code state inspection failed." >&2 | |
| return 1 | |
| fi | |
| return 0 | |
| } | |
| resolve_repo_dir() { | |
| if [[ -n "$REPO_DIR" ]]; then | |
| [[ -d "$REPO_DIR/.git" || -f "$REPO_DIR/.git" ]] || die "--repo does not point at a Git repository: $REPO_DIR" | |
| printf '%s\n' "$REPO_DIR" | |
| return 0 | |
| fi | |
| if git rev-parse --show-toplevel >/dev/null 2>&1; then | |
| git rev-parse --show-toplevel | |
| return 0 | |
| fi | |
| die "not inside a Git repository; pass --repo PATH or set T3_CHECKPOINTS_REPO_DIR in $CONFIG_FILE" | |
| } | |
| parse_command_and_options() { | |
| if (($# == 0)); then | |
| usage | |
| exit 0 | |
| fi | |
| COMMAND="$1" | |
| shift | |
| case "$COMMAND" in | |
| help|-h|--help) | |
| usage | |
| exit 0 | |
| ;; | |
| plan) | |
| APPLY=0 | |
| ;; | |
| apply) | |
| APPLY=1 | |
| ;; | |
| *) | |
| die "unknown command: $COMMAND" | |
| ;; | |
| esac | |
| if (($# > 0)) && [[ "$1" == "help" || "$1" == "--help" || "$1" == "-h" ]]; then | |
| usage | |
| exit 0 | |
| fi | |
| (($# > 0)) || die "missing mode for $COMMAND" | |
| MODE="$1" | |
| shift | |
| case "$MODE" in | |
| prune-chat-checkpoints) | |
| if (($# > 0)) && [[ "$1" =~ ^[0-9]+$ ]]; then | |
| KEEP="$1" | |
| shift | |
| fi | |
| ;; | |
| clear-chat-checkpoints) | |
| if (($# > 0)) && [[ "$1" =~ ^[0-9]+$ ]]; then | |
| PRUNE_DAYS="$1" | |
| shift | |
| fi | |
| ;; | |
| *) | |
| die "unknown mode for $COMMAND: $MODE" | |
| ;; | |
| esac | |
| while (($# > 0)); do | |
| case "$1" in | |
| --repo) | |
| (($# >= 2)) || die "--repo requires a value" | |
| REPO_DIR="$2" | |
| shift 2 | |
| ;; | |
| --no-gc) | |
| RUN_GC=0 | |
| shift | |
| ;; | |
| --codex-root) | |
| (($# >= 2)) || die "--codex-root requires a value" | |
| CODEX_ROOT="$2" | |
| shift 2 | |
| ;; | |
| --no-codex-inspect) | |
| INSPECT_CODEX=0 | |
| shift | |
| ;; | |
| --t3-db) | |
| (($# >= 2)) || die "--t3-db requires a value" | |
| T3CODE_DB_PATHS+=("$2") | |
| shift 2 | |
| ;; | |
| --no-t3code-inspect) | |
| INSPECT_T3CODE=0 | |
| shift | |
| ;; | |
| --help|-h) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| die "unknown option for $COMMAND $MODE: $1" | |
| ;; | |
| esac | |
| done | |
| } | |
| run_main() { | |
| validate_positive_integer "$KEEP" || die "prune-chat-checkpoints must be a positive integer" | |
| validate_positive_integer "$PRUNE_DAYS" || die "clear-chat-checkpoints must be a positive integer" | |
| if [[ "$MODE" == "prune-chat-checkpoints" ]] && ((KEEP < MIN_SAFE_KEEP_COMMITS)); then | |
| echo "Requested prune-chat-checkpoints=$KEEP is below the safe minimum; using $MIN_SAFE_KEEP_COMMITS instead." >&2 | |
| KEEP="$MIN_SAFE_KEEP_COMMITS" | |
| fi | |
| REPO_DIR="$(resolve_repo_dir)" | |
| populate_t3code_db_paths | |
| declare -A THREAD_ENTRIES=() | |
| declare -A THREAD_LATEST_TS=() | |
| declare -A T3CODE_CLASSIFICATION=() | |
| declare -A T3CODE_DETAILS=() | |
| local threads_found=0 | |
| local valid_refs=0 | |
| local kept_refs=0 | |
| local deleted_refs=0 | |
| local protected_refs=0 | |
| local protected_threads=0 | |
| local ignored_refs=0 | |
| local inactive_threads_matched=0 | |
| local inactive_refs_matched=0 | |
| local now_epoch inactive_cutoff_epoch latest_ts count refs_to_delete latest_ts_human | |
| local thread_id turn_number ref ref_ts latest_existing entry rest | |
| local t3code_existing_db_count=0 | |
| local t3code_all_existing_opened=0 | |
| now_epoch="$(date +%s)" | |
| inactive_cutoff_epoch=$((now_epoch - PRUNE_DAYS * 86400)) | |
| mapfile -t ALL_REFS < <(git -C "$REPO_DIR" for-each-ref --format='%(refname)%09%(creatordate:unix)' refs/t3/checkpoints) | |
| for ref_with_ts in "${ALL_REFS[@]}"; do | |
| ref="${ref_with_ts%%$'\t'*}" | |
| ref_ts="${ref_with_ts#*$'\t'}" | |
| [[ "$ref_ts" =~ ^[0-9]+$ ]] || ref_ts=0 | |
| if [[ "$ref" =~ ^refs/t3/checkpoints/([^/]+)/turn/([0-9]+)$ ]]; then | |
| thread_id="${BASH_REMATCH[1]}" | |
| turn_number="${BASH_REMATCH[2]}" | |
| THREAD_ENTRIES["$thread_id"]+="${turn_number}"$'\t'"${ref}"$'\t'"${ref_ts}"$'\n' | |
| latest_existing="${THREAD_LATEST_TS[$thread_id]:-0}" | |
| if ((ref_ts > latest_existing)); then | |
| THREAD_LATEST_TS["$thread_id"]="$ref_ts" | |
| fi | |
| valid_refs=$((valid_refs + 1)) | |
| else | |
| ignored_refs=$((ignored_refs + 1)) | |
| echo "Ignoring non-checkpoint-shaped ref under refs/t3/checkpoints: $ref" >&2 | |
| fi | |
| done | |
| threads_found="${#THREAD_ENTRIES[@]}" | |
| if ((threads_found == 0)); then | |
| echo "No T3 checkpoint refs found matching refs/t3/checkpoints/<thread-id>/turn/<integer>." | |
| echo "Repo: $REPO_DIR" | |
| echo "Threads found: 0" | |
| echo "Refs kept: 0" | |
| echo "Refs would delete: 0" | |
| exit 0 | |
| fi | |
| if ((APPLY)); then | |
| echo "Apply mode: $MODE" | |
| else | |
| echo "Report-only mode: $MODE" | |
| fi | |
| echo "Repo: $REPO_DIR" | |
| case "$MODE" in | |
| prune-chat-checkpoints) | |
| echo "Prune-chat-checkpoints cap: preserve turn 0 and keep up to $KEEP total checkpoint ref(s) per thread" | |
| ;; | |
| clear-chat-checkpoints) | |
| echo "Clear-chat-checkpoints cutoff: latest checkpoint older than $PRUNE_DAYS day(s)" | |
| echo "Inactive cutoff: $(format_epoch "$inactive_cutoff_epoch")" | |
| ;; | |
| esac | |
| THREAD_FILE="$(mktemp)" | |
| INSPECTION_FILE="$(mktemp)" | |
| trap 'rm -f "$THREAD_FILE" "$INSPECTION_FILE"' EXIT | |
| printf '%s\n' "${!THREAD_ENTRIES[@]}" | sort >"$THREAD_FILE" | |
| if ((INSPECT_T3CODE)); then | |
| echo | |
| echo "T3 Code state classification (authoritative safety check):" | |
| if inspect_t3code_state "$THREAD_FILE" "$INSPECTION_FILE"; then | |
| read -r t3code_existing_db_count t3code_all_existing_opened < <( | |
| python3 - "$INSPECTION_FILE" <<'PY' | |
| import json | |
| import sys | |
| payload = json.load(open(sys.argv[1])) | |
| print( | |
| payload.get("existingDbCount", 0), | |
| 1 if payload.get("allExistingDbPathsOpened", False) else 0, | |
| ) | |
| PY | |
| ) | |
| while IFS=$'\t' read -r thread_id classification details; do | |
| [[ -n "$thread_id" ]] || continue | |
| T3CODE_CLASSIFICATION["$thread_id"]="$classification" | |
| T3CODE_DETAILS["$thread_id"]="$details" | |
| done < <( | |
| python3 - "$INSPECTION_FILE" <<'PY' | |
| import json | |
| import sys | |
| from collections import Counter | |
| payload = json.load(open(sys.argv[1])) | |
| counts = Counter(record["classification"] for record in payload["records"]) | |
| for report in payload["dbReports"]: | |
| if report["status"] == "missing": | |
| continue | |
| line = f"DB {report['path']}: {report['status']}" | |
| if report.get("error"): | |
| line += f" ({report['error']})" | |
| print(line, file=sys.stderr) | |
| for key in ( | |
| "live-candidate", | |
| "archived-candidate", | |
| "runtime-bound-candidate", | |
| "lingering-state-candidate", | |
| "deleted-candidate", | |
| "orphaned-candidate", | |
| ): | |
| print(f"{key}: {counts.get(key, 0)}", file=sys.stderr) | |
| for record in payload["records"]: | |
| print(f"{record['threadId']}\t{record['classification']}\t{record['details']}") | |
| PY | |
| ) 2> >(sed 's/^/ /' >&2) | |
| for thread_id in "${!T3CODE_CLASSIFICATION[@]}"; do | |
| echo " - $thread_id: ${T3CODE_CLASSIFICATION[$thread_id]} (${T3CODE_DETAILS[$thread_id]})" | |
| done | |
| if ((APPLY)); then | |
| if ((t3code_existing_db_count == 0)); then | |
| die "No existing T3 Code state database could be inspected; refusing apply." | |
| fi | |
| if ((t3code_all_existing_opened == 0)); then | |
| die "Not every existing configured T3 Code database was inspected successfully; refusing apply." | |
| fi | |
| fi | |
| else | |
| if ((APPLY)); then | |
| die "T3 Code state inspection is unavailable; rerun with 'plan' or pass --no-t3code-inspect only if you intentionally want to bypass T3 Code safety guards." | |
| fi | |
| echo " T3 Code state inspection unavailable; continuing with ref-only report." | |
| fi | |
| else | |
| echo | |
| echo "T3 Code state classification skipped because --no-t3code-inspect was provided." | |
| if ((APPLY)); then | |
| echo "Warning: apply mode is running without T3 Code safety guards." >&2 | |
| fi | |
| fi | |
| if ((INSPECT_CODEX)); then | |
| echo | |
| echo "Codex-state candidate classification (advisory only):" | |
| if inspect_codex_state "$THREAD_FILE" "$INSPECTION_FILE"; then | |
| python3 - "$INSPECTION_FILE" <<'PY' | |
| import json | |
| import sys | |
| from collections import Counter | |
| payload = json.load(open(sys.argv[1])) | |
| print(f" Codex root: {payload['codexRoot']}") | |
| print(f" SQLite status: {payload['dbStatus']}") | |
| if payload.get("dbError"): | |
| print(f" SQLite error: {payload['dbError']}") | |
| print(f" Session metadata status: {payload['sessionStatus']}") | |
| counts = Counter(record["classification"] for record in payload["records"]) | |
| for key in ("active-candidate", "archived-candidate", "orphaned-candidate"): | |
| print(f" {key}: {counts.get(key, 0)}") | |
| for record in payload["records"]: | |
| print(f" - {record['threadId']}: {record['classification']} ({record['details']})") | |
| PY | |
| else | |
| echo " Codex-state inspection unavailable; continuing without advisory Codex metadata." | |
| fi | |
| else | |
| echo | |
| echo "Codex-state candidate classification skipped because --no-codex-inspect was provided." | |
| fi | |
| echo | |
| while IFS= read -r thread_id; do | |
| [[ -n "$thread_id" ]] || continue | |
| mapfile -t sorted_entries < <( | |
| printf '%s' "${THREAD_ENTRIES[$thread_id]}" | awk 'NF' | sort -t $'\t' -k1,1n | |
| ) | |
| count="${#sorted_entries[@]}" | |
| if ((count == 0)); then | |
| continue | |
| fi | |
| latest_ts="${THREAD_LATEST_TS[$thread_id]:-0}" | |
| latest_ts_human="$(format_epoch "$latest_ts")" | |
| echo "Thread $thread_id: $count checkpoint ref(s), latest checkpoint $latest_ts_human" | |
| local t3code_classification t3code_details | |
| t3code_classification="${T3CODE_CLASSIFICATION[$thread_id]:-unknown}" | |
| t3code_details="${T3CODE_DETAILS[$thread_id]:-no T3 Code classification available}" | |
| echo " T3 Code state: $t3code_classification ($t3code_details)" | |
| case "$t3code_classification" in | |
| live-candidate|archived-candidate|runtime-bound-candidate|lingering-state-candidate) | |
| protected_threads=$((protected_threads + 1)) | |
| protected_refs=$((protected_refs + count)) | |
| kept_refs=$((kept_refs + count)) | |
| echo " keeping all $count ref(s); protected by T3 Code state" | |
| continue | |
| ;; | |
| esac | |
| case "$MODE" in | |
| clear-chat-checkpoints) | |
| if ((latest_ts > 0)) && ((latest_ts < inactive_cutoff_epoch)); then | |
| inactive_threads_matched=$((inactive_threads_matched + 1)) | |
| inactive_refs_matched=$((inactive_refs_matched + count)) | |
| if ((APPLY)); then | |
| echo " deleting all $count ref(s); thread is inactive past the cutoff" | |
| else | |
| echo " would delete all $count ref(s); thread is inactive past the cutoff" | |
| fi | |
| for entry in "${sorted_entries[@]}"; do | |
| turn_number="${entry%%$'\t'*}" | |
| rest="${entry#*$'\t'}" | |
| ref="${rest%%$'\t'*}" | |
| if ((APPLY)); then | |
| echo " deleting turn $turn_number: $ref" | |
| git -C "$REPO_DIR" update-ref -d "$ref" | |
| else | |
| echo " would delete turn $turn_number: $ref" | |
| fi | |
| deleted_refs=$((deleted_refs + 1)) | |
| done | |
| continue | |
| fi | |
| kept_refs=$((kept_refs + count)) | |
| echo " keeping all $count ref(s)" | |
| ;; | |
| prune-chat-checkpoints) | |
| mapfile -t deletable_entries < <( | |
| printf '%s\n' "${sorted_entries[@]}" | awk -F $'\t' '$1 != "0"' | |
| ) | |
| local deletable_count refs_to_delete_effective | |
| deletable_count="${#deletable_entries[@]}" | |
| if ((count <= KEEP)) || ((deletable_count == 0)); then | |
| kept_refs=$((kept_refs + count)) | |
| echo " keeping all $count ref(s)" | |
| continue | |
| fi | |
| refs_to_delete=$((count - KEEP)) | |
| refs_to_delete_effective=$refs_to_delete | |
| if ((refs_to_delete_effective > deletable_count)); then | |
| refs_to_delete_effective=$deletable_count | |
| fi | |
| kept_refs=$((count - refs_to_delete_effective)) | |
| if ((APPLY)); then | |
| echo " deleting $refs_to_delete_effective older ref(s); keeping turn 0 and the newest remaining refs" | |
| else | |
| echo " would delete $refs_to_delete_effective older ref(s); keeping turn 0 and the newest remaining refs" | |
| fi | |
| for ((i = 0; i < refs_to_delete_effective; i++)); do | |
| entry="${deletable_entries[$i]}" | |
| turn_number="${entry%%$'\t'*}" | |
| rest="${entry#*$'\t'}" | |
| ref="${rest%%$'\t'*}" | |
| if ((APPLY)); then | |
| echo " deleting turn $turn_number: $ref" | |
| git -C "$REPO_DIR" update-ref -d "$ref" | |
| else | |
| echo " would delete turn $turn_number: $ref" | |
| fi | |
| deleted_refs=$((deleted_refs + 1)) | |
| done | |
| ;; | |
| esac | |
| done <"$THREAD_FILE" | |
| echo | |
| echo "Summary:" | |
| echo " Threads found: $threads_found" | |
| echo " Valid checkpoint refs: $valid_refs" | |
| echo " Refs kept: $kept_refs" | |
| if ((APPLY)); then | |
| echo " Refs deleted: $deleted_refs" | |
| else | |
| echo " Refs would delete: $deleted_refs" | |
| fi | |
| if ((ignored_refs > 0)); then | |
| echo " Ignored non-matching refs: $ignored_refs" | |
| fi | |
| echo " Protected threads: $protected_threads" | |
| echo " Protected refs kept due to T3 Code state: $protected_refs" | |
| if [[ "$MODE" == "clear-chat-checkpoints" ]]; then | |
| echo " Inactive threads matched: $inactive_threads_matched" | |
| echo " Refs deleted via inactive-thread mode: $inactive_refs_matched" | |
| echo " Threads remaining after inactive prune: $((threads_found - inactive_threads_matched))" | |
| fi | |
| if ((APPLY == 0)); then | |
| echo "Report-only mode: skipping ref deletion, reflog expiry, and git gc." | |
| exit 0 | |
| fi | |
| if ((deleted_refs == 0)); then | |
| echo "No refs deleted; skipping reflog expiry and git gc." | |
| exit 0 | |
| fi | |
| if ((RUN_GC == 0)); then | |
| echo "Skipping reflog expiry and git gc because --no-gc was provided." | |
| exit 0 | |
| fi | |
| echo "Expiring reflogs and pruning unreachable objects..." | |
| git -C "$REPO_DIR" reflog expire --expire=now --all | |
| git -C "$REPO_DIR" gc --prune=now | |
| echo "Finished pruning T3 checkpoint refs." | |
| } | |
| parse_command_and_options "$@" | |
| run_main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| exec /home/ben/bin/t3-checkpoints "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| _t3_checkpoints() { | |
| local cur prev | |
| COMPREPLY=() | |
| cur="${COMP_WORDS[COMP_CWORD]}" | |
| prev="${COMP_WORDS[COMP_CWORD-1]}" | |
| local common_opts="--repo --no-gc --codex-root --no-codex-inspect --t3-db --no-t3code-inspect --help" | |
| if (( COMP_CWORD == 1 )); then | |
| COMPREPLY=($(compgen -W "plan apply help" -- "$cur")) | |
| return 0 | |
| fi | |
| case "$prev" in | |
| plan|apply) | |
| COMPREPLY=($(compgen -W "prune-chat-checkpoints clear-chat-checkpoints" -- "$cur")) | |
| return 0 | |
| ;; | |
| prune-chat-checkpoints) | |
| COMPREPLY=($(compgen -W "1 3 5 10 20 $common_opts" -- "$cur")) | |
| return 0 | |
| ;; | |
| clear-chat-checkpoints) | |
| COMPREPLY=($(compgen -W "1 3 5 7 14 30 $common_opts" -- "$cur")) | |
| return 0 | |
| ;; | |
| --repo|--codex-root) | |
| compopt -o filenames 2>/dev/null | |
| COMPREPLY=($(compgen -d -- "$cur")) | |
| return 0 | |
| ;; | |
| --t3-db) | |
| compopt -o filenames 2>/dev/null | |
| COMPREPLY=($(compgen -f -- "$cur")) | |
| return 0 | |
| ;; | |
| esac | |
| COMPREPLY=($(compgen -W "$common_opts" -- "$cur")) | |
| } | |
| complete -F _t3_checkpoints t3-checkpoints t3cp |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment