#!/usr/bin/env bash # Duiliu Codex setup, macOS / Linux. Only an API key is required. set -euo pipefail umask 077 fail() { printf '\n%s\n' "$*" >&2; exit 1; } case "$(uname -s)" in Linux) platform=linux ;; Darwin) platform=darwin ;; *) fail 'Use https://app.duiliu.uk/install.ps1 on Windows.' ;; esac case "$(uname -m)" in x86_64|amd64) arch=x64 ;; arm64|aarch64) arch=arm64 ;; *) fail 'This installer supports x64 and ARM64 computers.' ;; esac for tool in curl tar; do command -v "$tool" >/dev/null 2>&1 || fail "Required command is missing: $tool" done if command -v sha256sum >/dev/null 2>&1; then sha256() { sha256sum "$1" | awk '{print $1}'; } elif command -v shasum >/dev/null 2>&1; then sha256() { shasum -a 256 "$1" | awk '{print $1}'; } else fail 'A SHA-256 tool (sha256sum or shasum) is required.' fi state_dir="$HOME/.local/share/duiliu" node_dir="$state_dir/node" runtime_dir="$state_dir/codex" config_dir="$HOME/.codex-duiliu" bin_dir="$HOME/.local/bin" launcher="$bin_dir/duiliu" marker='# managed by duiliu-codex-installer' for dest in "$state_dir" "$node_dir" "$runtime_dir" "$config_dir" "$bin_dir" "$launcher" "$config_dir/config.toml" "$config_dir/api-key"; do [[ ! -L "$dest" ]] || fail "Symlinked destination is not supported: $dest" done if [[ -e "$launcher" ]] && ! grep -Fq "$marker" "$launcher"; then fail "The command $launcher already exists and is not managed by this installer." fi printf 'Duiliu Codex setup (macOS / Linux)\n' [[ -r /dev/tty ]] || fail 'Run this command in an interactive terminal.' IFS= read -r -s -p 'Enter your Duiliu API Key (hidden): ' api_key key+=x); process.stdin.on("end", async()=>{try { const response=await fetch("https://app.duiliu.uk/v1/models",{headers:{Authorization:"Bearer "+key},signal:AbortSignal.timeout(20000)}); if(!response.ok) throw Error(response.status===401?"API key rejected (401).":response.status===429?"Too many requests (429). Retry shortly.":"Model list failed: HTTP "+response.status); const payload=await response.json(); if(!Array.isArray(payload.data)) throw Error("Invalid model list received."); const ids=new Set(payload.data.map(x=>x.id)); const model=["gpt-6-astra","gpt-6.1-sol","gpt-6-sol","gpt-6-luna"].find(x=>ids.has(x)); if(!model) throw Error("This key has no supported Codex model. Check its group and model permissions."); process.stdout.write(model); } catch(error) { console.error(error.message); process.exitCode=1; } }); ')" || fail 'API key validation failed. Check the key and network, then rerun.' printf 'Installing / updating the official Codex CLI...\n' "$node_dir/bin/node" "$node_dir/lib/node_modules/npm/bin/npm-cli.js" install --prefix "$runtime_dir" --registry=https://registry.npmjs.org --no-audit --no-fund --progress=false '@openai/codex@latest' codex_entry="$runtime_dir/node_modules/@openai/codex/bin/codex.js" "$node_dir/bin/node" "$codex_entry" --version backup_dir="$config_dir/backups/$(date +%Y%m%d-%H%M%S)-$$" if [[ -e "$config_dir/config.toml" || -e "$config_dir/api-key" || -e "$launcher" ]]; then mkdir -p "$backup_dir" for old in "$config_dir/config.toml" "$config_dir/api-key" "$launcher"; do [[ ! -e "$old" ]] || cp -p "$old" "$backup_dir/" done printf 'Previous Duiliu configuration backed up: %s\n' "$backup_dir" fi key_stage="$(mktemp "$config_dir/.key.XXXXXXXX")" printf '%s' "$api_key" > "$key_stage" chmod 600 "$key_stage" mv "$key_stage" "$config_dir/api-key" unset api_key config_stage="$(mktemp "$config_dir/.config.XXXXXXXX")" cat > "$config_stage" < "$launcher_stage" <<'LAUNCHER' #!/usr/bin/env bash # managed by duiliu-codex-installer set -euo pipefail config_dir="$HOME/.codex-duiliu" node_dir="$HOME/.local/share/duiliu/node" codex_entry="$HOME/.local/share/duiliu/codex/node_modules/@openai/codex/bin/codex.js" [[ -s "$config_dir/api-key" ]] || { printf 'API key is missing. Rerun the Duiliu installer.\n' >&2; exit 1; } # Scope CODEX_HOME to this child process, without changing existing Codex sessions. export CODEX_HOME="$config_dir" export DUILIU_API_KEY="$(cat "$config_dir/api-key")" export PATH="$node_dir/bin:$PATH" # Opt in per launch. Use the native Codex Fast setting, also controlled by /fast. if [[ "${1:-}" == 'fast' || "${1:-}" == '--fast' ]]; then shift set -- -c 'service_tier="fast"' "$@" fi exec "$node_dir/bin/node" "$codex_entry" "$@" LAUNCHER chmod 700 "$launcher_stage" mv "$launcher_stage" "$launcher" # Install a PATH line for the user's usual login and interactive shells. path_line='export PATH="$HOME/.local/bin:$PATH" # duiliu-codex-path' case "${SHELL:-/bin/bash}" in */zsh) shell_files=("$HOME/.zshrc" "$HOME/.zprofile") ;; */bash) shell_files=("$HOME/.bashrc") if [[ -f "$HOME/.bash_profile" ]]; then shell_files+=("$HOME/.bash_profile") elif [[ -f "$HOME/.bash_login" ]]; then shell_files+=("$HOME/.bash_login") else shell_files+=("$HOME/.profile"); fi ;; *) shell_files=("$HOME/.profile") ;; esac for shell_file in "${shell_files[@]}"; do if ! grep -Fq '# duiliu-codex-path' "$shell_file" 2>/dev/null; then if [[ -f "$shell_file" ]]; then mkdir -p "$backup_dir" cp -p "$shell_file" "$backup_dir/$(basename "$shell_file")" fi printf '\n%s\n' "$path_line" >> "$shell_file" fi done printf '\nSetup complete. Model: %s\n' "$model" printf 'Start now: %q\n' "$launcher" printf 'In a new terminal, type: duiliu\n' printf 'Optional Fast mode: duiliu fast\n' printf 'Change your API key: rerun this installer.\n'