#!/usr/bin/env bash
set -uo pipefail
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SRC_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
AGENTS_MD_SIDECAR="AGENTS.scholar.md"
AGENTS_ZH_MD_SIDECAR="AGENTS.zh-CN.scholar.md"
BACKUP_ROOT="$CODEX_HOME/.codex-scholar-backups"
MANIFEST_FILE="$CODEX_HOME/.codex-scholar-manifest.txt"
STATE_FILE="$CODEX_HOME/.codex-scholar-install-state"
PREVIOUS_MANAGED_PATHS_FILE="$(mktemp)"
BACKUP_STAMP="$(date +%Y%m%d-%H%M%S)"
BACKUP_DIR="$BACKUP_ROOT/$BACKUP_STAMP"
BACKUP_READY=0
BACKUP_COUNT=0
UPDATED_COUNT=0
SKIPPED_COUNT=0
CONFIG_CREATED=0
CONFIG_SHA256=""
MANAGED_PATHS=()
AGENTS_TARGETS=()
CONFIG_META_FILE="$(mktemp)"
LEGACY_INSTALL_DETECTED=0
SKIP_PROVIDER=false
SKIP_AUTH=false
PERSIST_AUTH=false
ENV_AUTH_DETECTED=0
PROVIDER_NAME=""
PROVIDER_URL=""
MODEL=""
AUTH_ENV_VAR_NAME="OPENAI_API_KEY"
API_KEY=""
SCHOLAR_DEBUG="${SCHOLAR_DEBUG:-0}"
INSTALL_STEP=""
FIND_CMD=""
green() { printf "\033[32m%s\033[0m" "$1"; }
red() { printf "\033[31m%s\033[0m" "$1"; }
yellow() { printf "\033[33m%s\033[0m" "$1"; }
bold() { printf "\033[1m%s\033[0m" "$1"; }
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
error() {
echo -e "\033[1;31m[ERROR]\033[0m $*"
if [ "$SCHOLAR_DEBUG" = "1" ]; then
debug "error: step=${INSTALL_STEP:-none} line=${BASH_LINENO[0]:-unknown}"
fi
exit 1
}
debug() {
[ "$SCHOLAR_DEBUG" = "1" ] || return 0
printf '[DEBUG] %s\n' "$*" >&2
}
debug_state() {
[ "$SCHOLAR_DEBUG" = "1" ] || return 0
debug "state: CODEX_HOME=$CODEX_HOME"
debug "state: SRC_DIR=$SRC_DIR"
debug "state: SKIP_PROVIDER=$SKIP_PROVIDER SKIP_AUTH=$SKIP_AUTH PERSIST_AUTH=$PERSIST_AUTH ENV_AUTH_DETECTED=$ENV_AUTH_DETECTED"
debug "state: PROVIDER_NAME=${PROVIDER_NAME:-<empty>} MODEL=${MODEL:-<empty>}"
debug "state: config_exists=$([ -f "$CODEX_HOME/config.toml" ] && printf yes || printf no) auth_exists=$([ -f "$CODEX_HOME/auth.json" ] && printf yes || printf no) manifest_exists=$([ -f "$MANIFEST_FILE" ] && printf yes || printf no)"
}
run_step() {
local step_name="$1"
shift
INSTALL_STEP="$step_name"
debug "step:start $step_name"
debug_state
"$@"
local rc=$?
if [ "$rc" -ne 0 ]; then
error "Step failed: $step_name (exit=$rc)"
fi
debug "step:done $step_name"
INSTALL_STEP=""
}
normalize_host_path() {
local path_value="$1"
if command -v cygpath >/dev/null 2>&1; then
cygpath -m "$path_value"
else
printf '%s' "$path_value"
fi
}
select_find_cmd() {
if [ -x /usr/bin/find ]; then
FIND_CMD="/usr/bin/find"
elif command -v gfind >/dev/null 2>&1; then
FIND_CMD="$(command -v gfind)"
elif find . -maxdepth 0 -print0 >/dev/null 2>&1; then
FIND_CMD="$(command -v find)"
else
error "A Unix-compatible find command is required."
fi
debug "using find command: $FIND_CMD"
}
read_prompt() {
local __var="$1"
local prompt="$2"
local default_value="${3:-}"
local silent="${4:-0}"
local value=""
if [ "$silent" = "1" ]; then
if read -rsp "$prompt" value; then
echo ""
else
echo ""
value="$default_value"
fi
else
if ! read -rp "$prompt" value; then
value="$default_value"
fi
fi
if [ -z "$value" ]; then
value="$default_value"
fi
printf -v "$__var" '%s' "$value"
}
cleanup_temp_files() {
rm -f "$CONFIG_META_FILE" "$PREVIOUS_MANAGED_PATHS_FILE"
}
on_exit() {
local rc=$?
if [ "$SCHOLAR_DEBUG" = "1" ]; then
debug "exit: rc=$rc step=${INSTALL_STEP:-none} line=${LINENO}"
debug "summary: updated=$UPDATED_COUNT skipped=$SKIPPED_COUNT backups=$BACKUP_COUNT"
fi
cleanup_temp_files
}
trap on_exit EXIT
declare -a PRESET_NAMES=("openai" "custom")
declare -a PRESET_LABELS=("OpenAI (official)" "Custom provider")
declare -a PRESET_URLS=("https://api.openai.com/v1" "")
declare -a PRESET_MODELS=("gpt-5.4" "")
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--debug|-d)
SCHOLAR_DEBUG=1
shift
;;
--help|-h)
cat <<'EOF'
Usage: bash scripts/setup.sh [--debug]
Options:
--debug, -d Enable verbose phase/state logging.
--help, -h Show this help.
You can also enable debug with:
SCHOLAR_DEBUG=1 bash scripts/setup.sh
EOF
exit 0
;;
*)
error "Unknown argument: $1"
;;
esac
done
}
load_previous_manifest() {
if [ -f "$MANIFEST_FILE" ]; then
cp "$MANIFEST_FILE" "$PREVIOUS_MANAGED_PATHS_FILE" || error "Failed to copy previous install manifest"
else
: > "$PREVIOUS_MANAGED_PATHS_FILE" || error "Failed to initialize previous manifest cache"
fi
}
detect_legacy_install() {
local config="$CODEX_HOME/config.toml"
[ -f "$MANIFEST_FILE" ] && return 0
[ -f "$config" ] || return 0
if grep -q 'config_file = "~/.codex/agents/' "$config" 2>/dev/null; then
LEGACY_INSTALL_DETECTED=1
fi
}
record_managed_path() {
local target="$1"
local rel="${target#$CODEX_HOME/}"
[ "$rel" = "$target" ] && return 0
[ -z "$rel" ] && return 0
MANAGED_PATHS+=("$rel")
}
record_agents_target() {
local target="$1"
local rel="${target#$CODEX_HOME/}"
[ "$rel" = "$target" ] && return 0
[ -z "$rel" ] && return 0
AGENTS_TARGETS+=("$rel")
}
was_previously_managed() {
local target="$1"
local rel="${target#$CODEX_HOME/}"
[ "$rel" = "$target" ] && return 1
grep -Fxq "$rel" "$PREVIOUS_MANAGED_PATHS_FILE"
}
should_adopt_existing_path() {
local target="$1"
if was_previously_managed "$target"; then
return 0
fi
if is_existing_scholar_agents_file "$target"; then
return 0
fi
[ "$LEGACY_INSTALL_DETECTED" -eq 1 ]
}
is_existing_scholar_agents_file() {
local target="$1"
local rel="${target#$CODEX_HOME/}"
[ "$rel" = "$target" ] && return 1
case "$rel" in
AGENTS.md|AGENTS.zh-CN.md) ;;
*) return 1 ;;
esac
[ -f "$target" ] || return 1
head -n 40 "$target" | grep -Eq '^# (Codex Scholar|Claude Scholar) (Core Instructions|核心指令)$'
}
file_sha256() {
local target="$1"
if command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$target" | awk '{print $1}'
elif command -v sha256sum >/dev/null 2>&1; then
sha256sum "$target" | awk '{print $1}'
else
printf ''
fi
}
write_config_meta() {
local config_created="$1"
local sections_csv="$2"
local created_json="false"
local first=1
local section=""
if [ "$config_created" = "true" ]; then
created_json="true"
fi
{
printf '{\n'
printf ' "configCreated": %s,\n' "$created_json"
printf ' "addedSections": ['
local IFS=','
for section in $sections_csv; do
[ -n "$section" ] || continue
if [ "$first" -eq 1 ]; then
first=0
else
printf ', '
fi
printf '"%s"' "$section"
done
printf ']\n'
printf '}\n'
} > "$CONFIG_META_FILE" || error "Failed to write config metadata"
}
write_unique_lines() {
local target="$1"
shift
if [ "$#" -gt 0 ]; then
printf "%s\n" "$@" | awk 'NF && !seen[$0]++' > "$target" || return 1
else
: > "$target" || return 1
fi
}
join_lines_csv() {
awk 'BEGIN { first = 1 } NF { if (!first) printf ","; printf "%s", $0; first = 0 }'
}
write_install_state() {
mkdir -p "$CODEX_HOME" || error "Failed to create CODEX_HOME at $CODEX_HOME"
write_unique_lines "$MANIFEST_FILE" "${MANAGED_PATHS[@]}" || error "Failed to write install manifest"
local managed_paths_file agents_targets_file
managed_paths_file="$(mktemp)"
agents_targets_file="$(mktemp)"
write_unique_lines "$managed_paths_file" "${MANAGED_PATHS[@]}" || error "Failed to write managed paths temp file"
write_unique_lines "$agents_targets_file" "${AGENTS_TARGETS[@]}" || error "Failed to write agents targets temp file"
CODEX_STATE_FILE="$(normalize_host_path "$STATE_FILE")" \
CODEX_CONFIG_META_FILE="$(normalize_host_path "$CONFIG_META_FILE")" \
CODEX_MANAGED_PATHS_FILE="$(normalize_host_path "$managed_paths_file")" \
CODEX_AGENTS_TARGETS_FILE="$(normalize_host_path "$agents_targets_file")" \
CODEX_INSTALLED_AT="$BACKUP_STAMP" \
CODEX_SOURCE_DIR="$(normalize_host_path "$SRC_DIR")" \
CODEX_CONFIG_CREATED="$CONFIG_CREATED" \
CODEX_CONFIG_SHA256="$CONFIG_SHA256" \
CODEX_BACKUP_DIR="$(normalize_host_path "$BACKUP_DIR")" \
CODEX_BACKUP_READY="$BACKUP_READY" \
node <<'NODE'
const fs = require('fs');
function readLines(path) {
if (!path || !fs.existsSync(path)) return [];
return fs.readFileSync(path, 'utf8').split('\n').map((line) => line.trim()).filter(Boolean);
}
function readJson(path) {
if (!path || !fs.existsSync(path)) return {};
return JSON.parse(fs.readFileSync(path, 'utf8'));
}
const state = {
installedAt: process.env.CODEX_INSTALLED_AT,
sourceDir: process.env.CODEX_SOURCE_DIR,
configCreated: process.env.CODEX_CONFIG_CREATED === '1',
configSha256: process.env.CODEX_CONFIG_SHA256 || '',
backupDir: process.env.CODEX_BACKUP_READY === '1' ? process.env.CODEX_BACKUP_DIR : '',
managedPaths: readLines(process.env.CODEX_MANAGED_PATHS_FILE),
agentsTargets: readLines(process.env.CODEX_AGENTS_TARGETS_FILE),
config: readJson(process.env.CODEX_CONFIG_META_FILE),
};
fs.writeFileSync(process.env.CODEX_STATE_FILE, JSON.stringify(state, null, 2) + '\n');
NODE
local node_rc=$?
rm -f "$managed_paths_file" "$agents_targets_file"
[ "$node_rc" -eq 0 ] || error "Failed to write install state"
}
ensure_backup_dir() {
if [ "$BACKUP_READY" -eq 0 ]; then
mkdir -p "$BACKUP_DIR" || error "Failed to create backup directory $BACKUP_DIR"
BACKUP_READY=1
info "Backup directory: $BACKUP_DIR"
fi
}
backup_path() {
local target="$1"
[ -e "$target" ] || return 0
ensure_backup_dir
local rel="${target#$CODEX_HOME/}"
if [ "$rel" = "$target" ]; then
rel="$(basename "$target")"
fi
mkdir -p "$BACKUP_DIR/$(dirname "$rel")" || error "Failed to create backup parent for $rel"
if [ -d "$target" ]; then
cp -R "$target" "$BACKUP_DIR/$rel" || error "Failed to back up directory $target"
else
cp -p "$target" "$BACKUP_DIR/$rel" || error "Failed to back up file $target"
fi
debug "backup: ${target#$CODEX_HOME/} -> $BACKUP_DIR/$rel"
BACKUP_COUNT=$((BACKUP_COUNT + 1))
}
ensure_parent_dir() {
local target_path="$1"
local parent_dir
parent_dir="$(dirname "$target_path")"
if [ -e "$parent_dir" ] && [ ! -d "$parent_dir" ]; then
backup_path "$parent_dir"
rm -f "$parent_dir" || error "Failed to remove non-directory parent $parent_dir"
fi
mkdir -p "$parent_dir" || error "Failed to create parent directory $parent_dir"
}
copy_file_safely() {
local src_file="$1"
local target_file="$2"
ensure_parent_dir "$target_file"
if [ -f "$target_file" ] && cmp -s "$src_file" "$target_file"; then
if should_adopt_existing_path "$target_file"; then
record_managed_path "$target_file"
fi
debug "copy:skip unchanged ${target_file#$CODEX_HOME/}"
SKIPPED_COUNT=$((SKIPPED_COUNT + 1))
return 0
fi
if [ -e "$target_file" ]; then
backup_path "$target_file"
if [ -d "$target_file" ]; then
rm -rf "$target_file" || error "Failed to remove directory target $target_file"
fi
fi
cp -p "$src_file" "$target_file" || error "Failed to copy $src_file to $target_file"
debug "copy:update ${target_file#$CODEX_HOME/}"
record_managed_path "$target_file"
UPDATED_COUNT=$((UPDATED_COUNT + 1))
}
copy_dir_safely() {
local src_dir="$1"
local target_dir="$2"
if [ -e "$target_dir" ] && [ ! -d "$target_dir" ]; then
backup_path "$target_dir"
rm -f "$target_dir" || error "Failed to remove non-directory target $target_dir"
fi
ensure_parent_dir "$target_dir/.dir"
mkdir -p "$target_dir" || error "Failed to create target directory $target_dir"
while IFS= read -r -d '' src_file; do
local rel="${src_file#$src_dir/}"
local target_file="$target_dir/$rel"
copy_file_safely "$src_file" "$target_file"
done < <("$FIND_CMD" "$src_dir" -type f -print0)
}
install_agents_md() {
local src_file="$1"
local target_file="$CODEX_HOME/AGENTS.md"
local sidecar_file="$CODEX_HOME/$AGENTS_MD_SIDECAR"
if [ -f "$target_file" ] && should_adopt_existing_path "$target_file"; then
copy_file_safely "$src_file" "$target_file"
record_agents_target "$target_file"
return 0
fi
if [ -f "$target_file" ]; then
warn "Preserving existing AGENTS.md"
copy_file_safely "$src_file" "$sidecar_file"
record_agents_target "$sidecar_file"
info "Installed repository AGENTS.md as $AGENTS_MD_SIDECAR"
return 0
fi
copy_file_safely "$src_file" "$target_file"
record_agents_target "$target_file"
}
install_agents_zh_md() {
local src_file="$1"
local target_file="$CODEX_HOME/AGENTS.zh-CN.md"
local sidecar_file="$CODEX_HOME/$AGENTS_ZH_MD_SIDECAR"
if [ -f "$target_file" ] && should_adopt_existing_path "$target_file"; then
copy_file_safely "$src_file" "$target_file"
record_agents_target "$target_file"
return 0
fi
if [ -f "$target_file" ]; then
warn "Preserving existing AGENTS.zh-CN.md"
copy_file_safely "$src_file" "$sidecar_file"
record_agents_target "$sidecar_file"
info "Installed repository AGENTS.zh-CN.md as $AGENTS_ZH_MD_SIDECAR"
return 0
fi
copy_file_safely "$src_file" "$target_file"
record_agents_target "$target_file"
}
validate_env_var_name() {
local name="$1"
[[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || error "Invalid env var name: $name"
}
read_auth_entry() {
local file="$1"
[ -f "$file" ] || return 0
python3 - "$(normalize_host_path "$file")" <<'PY'
import json
import pathlib
import sys
path = pathlib.Path(sys.argv[1])
try:
data = json.loads(path.read_text())
except Exception:
sys.exit(0)
for key, value in data.items():
if isinstance(value, str) and value:
print(f"{key}\t{value}")
break
PY
}
normalize_env_prefix() {
local raw="$1"
printf '%s' "$raw" \
| tr '[:lower:]-./' '[:upper:]___' \
| sed 's/[^A-Z0-9_]/_/g; s/__*/_/g; s/^_//; s/_$//'
}
collect_api_key_candidates() {
local provider="$1"
local normalized_provider=""
local -a candidates=()
if [ -n "$provider" ]; then
normalized_provider=$(normalize_env_prefix "$provider")
if [ -n "$normalized_provider" ]; then
candidates+=("${normalized_provider}_API_KEY")
fi
fi
candidates+=(
"OPENAI_API_KEY"
"ANTHROPIC_API_KEY"
"OPENROUTER_API_KEY"
"GEMINI_API_KEY"
"GOOGLE_API_KEY"
"DEEPSEEK_API_KEY"
"DASHSCOPE_API_KEY"
"SILICONFLOW_API_KEY"
"XAI_API_KEY"
"GROQ_API_KEY"
"MISTRAL_API_KEY"
"COHERE_API_KEY"
"TOGETHER_API_KEY"
"FIREWORKS_API_KEY"
"MOONSHOT_API_KEY"
"ZHIPU_API_KEY"
)
printf '%s\n' "${candidates[@]}" | awk '!seen[$0]++'
}
detect_existing_env_auth() {
local provider="$1"
local candidate=""
local value=""
ENV_AUTH_DETECTED=0
while IFS= read -r candidate; do
[ -n "$candidate" ] || continue
value="${!candidate:-}"
if [ -n "$value" ]; then
AUTH_ENV_VAR_NAME="$candidate"
API_KEY="$value"
PERSIST_AUTH=true
ENV_AUTH_DETECTED=1
info "No auth.json found; detected an API key in the environment and will persist it for Codex compatibility"
return 0
fi
done < <(collect_api_key_candidates "$provider") || true
return 0
}
check_deps() {
command -v git >/dev/null || error "Git is required."
command -v python3 >/dev/null || error "Python 3 is required."
select_find_cmd
if ! command -v codex >/dev/null; then
warn "Codex CLI not found. Install: npm i -g @openai/codex"
fi
}
detect_existing() {
echo ""
if [ -f "$CODEX_HOME/config.toml" ]; then
info "Existing config.toml found at $CODEX_HOME/config.toml"
local cur_model cur_provider
cur_model=$(grep '^model ' "$CODEX_HOME/config.toml" 2>/dev/null | head -1 | sed 's/.*= *"//;s/".*//' || true)
cur_provider=$(grep '^model_provider ' "$CODEX_HOME/config.toml" 2>/dev/null | head -1 | sed 's/.*= *"//;s/".*//' || true)
PROVIDER_NAME="$cur_provider"
[ -n "$cur_model" ] && info " Current model: $cur_model"
[ -n "$cur_provider" ] && info " Current provider: $cur_provider"
SKIP_PROVIDER=true
info "Detected existing provider/model configuration; keeping it without prompting"
fi
if [ -f "$CODEX_HOME/auth.json" ]; then
local auth_entry existing_key_name existing_key_value
auth_entry=$(read_auth_entry "$CODEX_HOME/auth.json")
if [ -n "$auth_entry" ]; then
IFS=$'\t' read -r existing_key_name existing_key_value <<< "$auth_entry"
AUTH_ENV_VAR_NAME="$existing_key_name"
info "Existing auth.json credential found; leaving it untouched"
else
info "Existing auth.json found; leaving it untouched"
fi
SKIP_AUTH=true
info "Detected existing authentication configuration; keeping it without prompting"
elif [ "$SKIP_PROVIDER" = true ]; then
SKIP_AUTH=true
detect_existing_env_auth "$PROVIDER_NAME"
if [ "$ENV_AUTH_DETECTED" -ne 1 ]; then
info "Existing Codex config detected; installer will not prompt for credentials or overwrite your current auth flow"
fi
fi
debug "detect_existing: complete"
}
choose_provider() {
if [ "$SKIP_PROVIDER" = true ]; then
return
fi
echo ""
bold "Select API provider:"
echo ""
for i in "${!PRESET_LABELS[@]}"; do
echo " $((i+1))) ${PRESET_LABELS[$i]}"
done
echo ""
local choice
read_prompt choice "Enter choice [1-2] (default: 1): " "1"
case "$choice" in
''|*[!0-9]*)
warn "Invalid provider choice '$choice'; using default: 1"
choice="1"
;;
esac
local idx=$((choice - 1))
if [ "$idx" -lt 0 ] || [ "$idx" -ge "${#PRESET_NAMES[@]}" ]; then
warn "Invalid provider choice '$choice'; using default: 1"
idx=0
fi
PROVIDER_NAME="${PRESET_NAMES[$idx]}"
PROVIDER_URL="${PRESET_URLS[$idx]}"
MODEL="${PRESET_MODELS[$idx]}"
if [ "$PROVIDER_NAME" = "custom" ]; then
read_prompt PROVIDER_NAME "Provider name: " ""
read_prompt PROVIDER_URL "Base URL: " ""
read_prompt MODEL "Model name: " ""
[ -n "$PROVIDER_NAME" ] || error "Provider name is required for custom provider."
[ -n "$PROVIDER_URL" ] || error "Base URL is required for custom provider."
[ -n "$MODEL" ] || error "Model name is required for custom provider."
else
echo ""
read_prompt input_model "Model name (default: $MODEL): " "$MODEL"
MODEL="$input_model"
fi
info "Provider: $PROVIDER_NAME | URL: $PROVIDER_URL | Model: $MODEL"
}
configure_api_key() {
if [ "$SKIP_AUTH" = true ]; then
return
fi
echo ""
read_prompt input_env_name "API key env var name (default: $AUTH_ENV_VAR_NAME): " "$AUTH_ENV_VAR_NAME"
AUTH_ENV_VAR_NAME="$input_env_name"
validate_env_var_name "$AUTH_ENV_VAR_NAME"
local env_value="${!AUTH_ENV_VAR_NAME:-}"
if [ -n "$env_value" ]; then
API_KEY="$env_value"
PERSIST_AUTH=true
info "Detected $AUTH_ENV_VAR_NAME in current environment; will reuse it without prompting for the key again"
return
fi
read_prompt API_KEY "Enter API key for $AUTH_ENV_VAR_NAME (or press Enter to skip): " "" "1"
if [ -z "$API_KEY" ]; then
warn "No API key set. Make sure $AUTH_ENV_VAR_NAME is available in your environment."
SKIP_AUTH=true
return
fi
PERSIST_AUTH=true
}
generate_fresh_config() {
local template="$1"
local target="$2"
local sections=""
sed -e "s|__MODEL__|$MODEL|g" \
-e "s|__PROVIDER_NAME__|$PROVIDER_NAME|g" \
-e "s|__PROVIDER_URL__|$PROVIDER_URL|g" \
"$template" > "$target" || error "Failed to render config.toml from template"
CONFIG_CREATED=1
CONFIG_SHA256="$(file_sha256 "$target")"
sections="$(
awk '
{
line = $0
sub(/\r$/, "", line)
if (line ~ /^\[[^]]+\]$/) {
print substr(line, 2, length(line) - 2)
}
}
' "$target" | join_lines_csv
)" || error "Failed to collect config metadata"
write_config_meta true "$sections"
info "Generated config.toml (model=$MODEL, provider=$PROVIDER_NAME)"
}
merge_scholar_config() {
local target="$1"
local template="$2"
local added_file
added_file="$(mktemp)"
append_template_section() {
local header="$1"
local block=""
if grep -Fq "[$header]" "$target"; then
return 0
fi
block="$(
awk -v wanted="[$header]" '
{
line = $0
sub(/\r$/, "", line)
}
line == wanted {
capture = 1
print line
next
}
capture && line ~ /^\[/ {
exit
}
capture {
print line
}
' "$template"
)"
if [ -n "$block" ]; then
printf '\n\n%s\n' "$block" >> "$target" || return 1
printf '%s\n' "$header" >> "$added_file" || return 1
fi
}
append_template_section "features" || return 1
append_template_section "mcp_servers.zotero" || return 1
append_template_section "mcp_servers.zotero.env" || return 1
while IFS= read -r header; do
[ -n "$header" ] || continue
append_template_section "$header" || return 1
done < <(
awk '
{
line = $0
sub(/\r$/, "", line)
if (line ~ /^\[agents\.[^]]+\]$/) {
print substr(line, 2, length(line) - 2)
}
}
' "$template"
)
if [ -s "$added_file" ]; then
join_lines_csv < "$added_file"
fi
rm -f "$added_file"
}
generate_config() {
local template="$SRC_DIR/config.toml"
local target="$CODEX_HOME/config.toml"
[ -f "$template" ] || error "Template config.toml not found at $template"
if [ -f "$target" ]; then
backup_path "$target"
cp "$target" "${target}.bak" || error "Failed to write config.toml.bak"
info "Backed up config.toml → config.toml.bak"
fi
if [ "$SKIP_PROVIDER" = true ]; then
local added
added=$(merge_scholar_config "$target" "$template") || error "Failed to merge Scholar config sections"
write_config_meta false "$added"
CONFIG_SHA256="$(file_sha256 "$target")"
if [ -n "$added" ]; then
info "Merged Scholar sections into existing config.toml: $added"
else
info "Config already had the required Scholar sections"
fi
else
generate_fresh_config "$template" "$target"
fi
}
write_auth() {
if [ "$PERSIST_AUTH" != true ]; then
return
fi
local target="$CODEX_HOME/auth.json"
if [ -f "$target" ]; then
backup_path "$target"
cp "$target" "${target}.bak" || error "Failed to write auth.json.bak"
info "Backed up auth.json → auth.json.bak"
fi
python3 - "$(normalize_host_path "$target")" "$AUTH_ENV_VAR_NAME" "$API_KEY" <<'PY'
import json
import pathlib
import sys
target = pathlib.Path(sys.argv[1])
env_name = sys.argv[2]
api_key = sys.argv[3]
payload = {env_name: api_key}
if env_name != "OPENAI_API_KEY":
payload["OPENAI_API_KEY"] = api_key
target.write_text(json.dumps(payload, indent=2) + "\n")
PY
[ "$?" -eq 0 ] || error "Failed to write auth.json"
chmod 600 "$target" || error "Failed to set auth.json permissions"
if [ "$AUTH_ENV_VAR_NAME" = "OPENAI_API_KEY" ]; then
info "Wrote auth.json (permissions: 600)"
else
info "Wrote auth.json with $AUTH_ENV_VAR_NAME and OPENAI_API_KEY for Codex compatibility (permissions: 600)"
fi
}
copy_components() {
if [ -d "$SRC_DIR/skills" ]; then
copy_dir_safely "$SRC_DIR/skills" "$CODEX_HOME/skills"
fi
if [ -d "$SRC_DIR/templates" ]; then
copy_dir_safely "$SRC_DIR/templates" "$CODEX_HOME/templates"
fi
if [ -d "$SRC_DIR/agents" ]; then
copy_dir_safely "$SRC_DIR/agents" "$CODEX_HOME/agents"
fi
if [ -f "$SRC_DIR/AGENTS.md" ]; then
install_agents_md "$SRC_DIR/AGENTS.md"
fi
if [ -f "$SRC_DIR/AGENTS.zh-CN.md" ]; then
install_agents_zh_md "$SRC_DIR/AGENTS.zh-CN.md"
fi
if [ -d "$SRC_DIR/scripts" ]; then
copy_dir_safely "$SRC_DIR/scripts" "$CODEX_HOME/scripts"
fi
if [ -d "$SRC_DIR/utils" ]; then
copy_dir_safely "$SRC_DIR/utils" "$CODEX_HOME/utils"
fi
info "Synced repo-managed Codex components"
}
configure_mcp() {
if ! grep -q '\[mcp_servers\.zotero\]' "$CODEX_HOME/config.toml" 2>/dev/null; then
return
fi
if awk '/\[mcp_servers\.zotero\]/{flag=1;next}/^\[/{flag=0}flag && /enabled = true/{found=1}END{exit(found?0:1)}' "$CODEX_HOME/config.toml"; then
info "Zotero MCP already enabled"
return
fi
echo ""
local enable_zotero=""
read_prompt enable_zotero "Enable Zotero MCP server? [y/N]: " ""
if [ -z "$enable_zotero" ] && [ ! -t 0 ]; then
info "Zotero MCP is available but installer is running non-interactively; leaving it disabled"
fi
if [ "$enable_zotero" = "y" ] || [ "$enable_zotero" = "Y" ]; then
local tmp_config
tmp_config="$(mktemp)"
awk '
{
line = $0
clean = line
sub(/\r$/, "", clean)
if (clean ~ /^\[/) {
in_zotero = (clean == "[mcp_servers.zotero]")
}
if (in_zotero && clean ~ /^enabled[[:space:]]*=[[:space:]]*false[[:space:]]*$/) {
sub(/false/, "true", line)
changed = 1
}
print line
}
END {
if (!changed) {
exit 1
}
}
' "$CODEX_HOME/config.toml" > "$tmp_config" || {
rm -f "$tmp_config"
error "Failed to enable Zotero MCP"
}
mv "$tmp_config" "$CODEX_HOME/config.toml" || error "Failed to replace config.toml after enabling Zotero MCP"
info "Zotero MCP enabled"
if ! command -v zotero-mcp >/dev/null 2>&1; then
warn "zotero-mcp not found. Install latest with: uv tool install --reinstall git+https://github.com/Galaxy-Dawn/zotero-mcp.git"
fi
fi
}
main() {
parse_args "$@"
echo ""
echo "╔══════════════════════════════════════╗"
echo "║ Claude Scholar Installer (Codex) ║"
echo "╚══════════════════════════════════════╝"
echo ""
run_step "check_deps" check_deps
run_step "load_previous_manifest" load_previous_manifest
run_step "detect_legacy_install" detect_legacy_install
info "Source: $SRC_DIR"
info "Target: $CODEX_HOME"
mkdir -p "$CODEX_HOME" || error "Failed to create CODEX_HOME at $CODEX_HOME"
run_step "detect_existing" detect_existing
run_step "choose_provider" choose_provider
run_step "configure_api_key" configure_api_key
run_step "generate_config" generate_config
run_step "write_auth" write_auth
run_step "copy_components" copy_components
run_step "configure_mcp" configure_mcp
run_step "write_install_state" write_install_state
echo ""
echo "============================================================"
info "Installation complete!"
info "Install manifest: $MANIFEST_FILE"
info "Updated files: $UPDATED_COUNT | Unchanged files skipped: $SKIPPED_COUNT | Backups created: $BACKUP_COUNT"
if [ "$BACKUP_READY" -eq 1 ]; then
info "Recover previous files from: $BACKUP_DIR"
fi
echo ""
echo " Config: $CODEX_HOME/config.toml"
echo " Auth: $CODEX_HOME/auth.json"
echo " Skills: $CODEX_HOME/skills/"
echo " Templates: $CODEX_HOME/templates/"
echo " Agents: $CODEX_HOME/agents/"
echo ""
info "Existing model/provider/API key settings are preserved when you choose the incremental update path."
echo " Run $(bold 'codex') to start."
echo "============================================================"
}
main "$@"