Skip to content

Instantly share code, notes, and snippets.

@benhook1013
Last active May 15, 2026 08:31
Show Gist options
  • Select an option

  • Save benhook1013/d18cd883e3b00837d7c55d8d4223b0cc to your computer and use it in GitHub Desktop.

Select an option

Save benhook1013/d18cd883e3b00837d7c55d8d4223b0cc to your computer and use it in GitHub Desktop.
WSL Codex/T3 profile switching helper and bash completion
#!/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 "$@"
_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
#!/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
#!/usr/bin/env bash
set -euo pipefail
exec /home/ben/bin/t3-checkpoints "$@"
_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