#!/usr/bin/env bash # Instalador Avello para Mac: configura o Claude Code e/ou o Codex para usar a sua chave Avello. # # curl -fsSL https://avello.online/instalar/mac | bash # # O que ele faz, nesta ordem: # 1. pergunta o que configurar (Claude Code, Codex, os dois) ou se é para desfazer; # 2. instala a ferramenta que faltar, pelo instalador oficial dela; # 3. pede a sua chave (a digitação não aparece) e testa em /v1/models, que não gasta # crédito nem começa o relógio do teste grátis; # 4. guarda uma cópia de cada arquivo antes de mudar (anotada em ~/.config/avello); # 5. grava a configuração. "Desfazer" devolve as cópias. # # Opções, para suporte e automação: # --ferramenta claude|codex|ambos --chave-em --sim --desfazer ORIGEM="https://avello.online" MODELO_CLAUDE="claude-sonnet-5" MODELO_CLAUDE_OPUS="claude-opus-5.5" MODELO_CLAUDE_HAIKU="claude-haiku-4.5" MODELO_CODEX="gpt-5.6-sol-medium" PASTA_AVELLO="$HOME/.config/avello" MANIFESTO="$PASTA_AVELLO/instalacao.txt" ARQUIVO_DA_CHAVE="$PASTA_AVELLO/chave" SETTINGS_CLAUDE="$HOME/.claude/settings.json" CONFIG_CODEX="$HOME/.codex/config.toml" TTY=/dev/tty DIR_LOCAL_BIN="$HOME/.local/bin" # O PATH de quem chamou, antes de qualquer ajuste nosso: é com ele que os instaladores oficiais rodam. PATH_DE_QUEM_CHAMOU="$PATH" LINHA_DO_PATH='export PATH="$HOME/.local/bin:$PATH"' LINHA_DA_CHAVE='export AVELLO_API_KEY="$(cat "$HOME/.config/avello/chave" 2>/dev/null)"' LINHA_ANTHROPIC='^[[:space:]]*(export[[:space:]]+)?ANTHROPIC_(BASE_URL|AUTH_TOKEN|API_KEY|MODEL|DEFAULT_[A-Z]+_MODEL)=' FERRAMENTA="" CHAVE_EM="" SIM=0 DESFAZER=0 CHAVE="" RESPOSTA="" ULTIMO_CODIGO="" INSTALOU_EM_LOCAL_BIN=0 diga() { printf '%s\n' "$*"; } ok() { printf ' ✓ %s\n' "$*"; } aviso() { printf ' ! %s\n' "$*"; } falha() { printf '\n ✗ %s\n\n' "$1" >&2; exit "${2:-1}"; } ler_opcoes() { while [ $# -gt 0 ]; do case "$1" in --ferramenta|--chave-em) [ $# -ge 2 ] || falha "falta o valor de $1" if [ "$1" = "--ferramenta" ]; then FERRAMENTA="$2"; else CHAVE_EM="$2"; fi shift 2 ;; --sim) SIM=1; shift ;; --desfazer) DESFAZER=1; shift ;; *) falha "opção desconhecida: $1" ;; esac done } tem_terminal() { { : < "$TTY"; } 2>/dev/null; } perguntar() { tem_terminal || falha "sem terminal para perguntar; use --ferramenta, --chave-em e --sim" printf '%s' "$1" > "$TTY" IFS= read -r RESPOSTA < "$TTY" || RESPOSTA="" } escolher() { [ -n "$FERRAMENTA" ] || [ "$DESFAZER" -eq 1 ] && return diga "O que você quer fazer?" diga " 1) Configurar o Claude Code" diga " 2) Configurar o Codex" diga " 3) Configurar os dois" diga " 4) Desfazer a instalação anterior da Avello" perguntar "Escolha [1-4]: " case "$RESPOSTA" in 1) FERRAMENTA=claude ;; 2) FERRAMENTA=codex ;; 3) FERRAMENTA=ambos ;; 4) DESFAZER=1 ;; *) falha "opção inválida: $RESPOSTA" ;; esac } limpar_chave() { printf '%s' "$1" | tr -d '\r\n\t '; } chave_valida() { [[ "$1" =~ ^sk-[A-Za-z0-9-]{8,}$ ]]; } # 0 = aceita, 2 = recusada, 1 = sem resposta da Avello. A chave vai num arquivo de cabeçalho # temporário (só você lê), nunca na linha de comando. testar_chave() { local cabecalho cabecalho="$(mktemp)" chmod 600 "$cabecalho" printf 'Authorization: Bearer %s\n' "$1" > "$cabecalho" ULTIMO_CODIGO="$(curl -sS -m 20 -o /dev/null -w '%{http_code}' -H @"$cabecalho" "$ORIGEM/v1/models" 2>/dev/null)" rm -f "$cabecalho" case "$ULTIMO_CODIGO" in 200) return 0 ;; 401|403) return 2 ;; *) return 1 ;; esac } obter_chave() { local tentativa resultado if [ -n "$CHAVE_EM" ]; then [ -r "$CHAVE_EM" ] || falha "não consegui ler $CHAVE_EM" CHAVE="$(limpar_chave "$(cat "$CHAVE_EM")")" chave_valida "$CHAVE" || falha "isso não parece uma chave Avello (ela começa com sk-). Nada foi alterado." 2 testar_chave "$CHAVE"; resultado=$? [ "$resultado" -eq 0 ] && { ok "chave aceita pela Avello"; return; } [ "$resultado" -eq 2 ] && falha "a Avello recusou essa chave. Nada foi alterado." 2 falha "não consegui falar com a Avello (código ${ULTIMO_CODIGO:-000}). Confira a internet. Nada foi alterado." fi for tentativa in 1 2 3; do tem_terminal || falha "sem terminal para pedir a chave; use --chave-em" printf 'Cole a sua chave Avello (a digitação não aparece) e tecle Enter: ' > "$TTY" IFS= read -r -s CHAVE < "$TTY" || CHAVE="" printf '\n' > "$TTY" CHAVE="$(limpar_chave "$CHAVE")" if ! chave_valida "$CHAVE"; then aviso "isso não parece uma chave Avello (ela começa com sk-). Tentativa $tentativa de 3." continue fi testar_chave "$CHAVE"; resultado=$? [ "$resultado" -eq 0 ] && { ok "chave aceita pela Avello"; return; } [ "$resultado" -eq 1 ] && falha "não consegui falar com a Avello (código ${ULTIMO_CODIGO:-000}). Confira a internet. Nada foi alterado." aviso "a Avello recusou essa chave: confira se copiou inteira ou se o teste já venceu. Tentativa $tentativa de 3." done falha "chave recusada três vezes. Nada foi alterado." 2 } # Antes de mudar um arquivo pela primeira vez: copia (ou anota que ele não existia). anotar_e_copiar() { local arq="$1" mkdir -p "$PASTA_AVELLO" && chmod 700 "$PASTA_AVELLO" touch "$MANIFESTO" grep -Fqx "copia|$arq" "$MANIFESTO" && return grep -Fqx "novo|$arq" "$MANIFESTO" && return if [ -e "$arq" ]; then cp -p "$arq" "$arq.avello-bak" || falha "não consegui copiar $arq antes de mudar" printf 'copia|%s\n' "$arq" >> "$MANIFESTO" else printf 'novo|%s\n' "$arq" >> "$MANIFESTO" fi } hash_de() { shasum -a 256 "$1" 2>/dev/null | cut -d' ' -f1; } # Depois de gravar um arquivo: anota o que a Avello deixou nele, para o Desfazer saber se alguém mexeu depois. anotar_escrito() { local arq="$1" { grep -Fv "escrito|$arq|" "$MANIFESTO"; printf 'escrito|%s|%s\n' "$arq" "$(hash_de "$arq")"; } > "$MANIFESTO.tmp" mv -f "$MANIFESTO.tmp" "$MANIFESTO" } # O arquivo no disco não é mais o que a Avello gravou (o cliente ou outro instalador mexeu depois). mudou_depois() { local gravado case "$1" in "$PASTA_AVELLO"/*) return 1 ;; esac gravado="$(grep -F "escrito|$1|" "$MANIFESTO" | tail -n 1 | cut -d'|' -f3)" [ -n "$gravado" ] && [ -e "$1" ] && [ "$(hash_de "$1")" != "$gravado" ] } # Devolve cada arquivo como era antes da Avello. O que mudou nele depois da instalação não some sem cópia: # o estado de agora fica em .antes-do-desfazer. desfazer() { local tipo arq guardados="" if [ ! -f "$MANIFESTO" ]; then diga "Não há instalação da Avello para desfazer."; exit 0; fi while IFS='|' read -r tipo arq _; do case "$tipo" in copia|novo) ;; *) continue ;; esac if mudou_depois "$arq"; then cp -p "$arq" "$arq.antes-do-desfazer" && guardados="$guardados $arq.antes-do-desfazer"$'\n' fi case "$tipo" in copia) [ -e "$arq.avello-bak" ] && mv -f "$arq.avello-bak" "$arq" && ok "voltou como era: $arq" ;; novo) [ -e "$arq" ] && rm -f "$arq" && ok "removido: $arq" ;; esac done < "$MANIFESTO" rm -rf "$PASTA_AVELLO" ok "pronto: a configuração de antes da Avello voltou." if [ -n "$guardados" ]; then aviso "estes arquivos mudaram depois da instalação; o que eles tinham agora ficou guardado em:" printf '%s' "$guardados" fi exit 0 } # Instala pelo instalador oficial (comando, nome, url, interpretador, fabricante). Com pipefail: sem ele, um # download que cai vira um shell rodando vazio, que sai 0. Com o PATH de quem chamou: com o nosso, o instalador do # Codex acha ~/.local/bin no PATH e não grava o perfil do shell, e o terminal novo não acha o programa. garantir() { if command -v "$1" >/dev/null 2>&1; then ok "$2 já instalado"; return; fi diga " Instalando o $2 pelo instalador oficial da $5..." ( set -o pipefail; curl -fsSL "$3" | PATH="$PATH_DE_QUEM_CHAMOU" "$4" ) \ || falha "o instalador oficial do $2 falhou. Nenhuma configuração foi alterada." export PATH="$DIR_LOCAL_BIN:$PATH" command -v "$1" >/dev/null 2>&1 \ || falha "o instalador oficial do $2 terminou, mas o $1 não apareceu em $DIR_LOCAL_BIN. Nenhuma configuração foi alterada." INSTALOU_EM_LOCAL_BIN=1 ok "$2 instalado" } garantir_claude() { garantir claude "Claude Code" https://claude.ai/install.sh bash Anthropic; } garantir_codex() { garantir codex Codex https://chatgpt.com/codex/install.sh sh OpenAI; } limpar_anthropic_do_shell() { local arq lista="" for arq in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do [ -f "$arq" ] && grep -Eq "$LINHA_ANTHROPIC" "$arq" && lista="$lista$arq"$'\n' done [ -z "$lista" ] && return aviso "estas linhas definem ANTHROPIC_* e passariam por cima da configuração da Avello:" while IFS= read -r arq; do [ -n "$arq" ] && grep -nE "$LINHA_ANTHROPIC" "$arq" | sed -E 's/=.*/=…/' | sed "s|^| $arq:|" done <<< "$lista" if [ "$SIM" -ne 1 ]; then if ! tem_terminal; then aviso "mantive as linhas (sem terminal para perguntar)"; return; fi perguntar " Remover essas linhas? Uma cópia é guardada. [s/N] " case "$RESPOSTA" in s|S|sim|Sim|SIM) ;; *) aviso "mantive as linhas"; return ;; esac fi while IFS= read -r arq; do [ -n "$arq" ] || continue anotar_e_copiar "$arq" grep -Ev "$LINHA_ANTHROPIC" "$arq" > "$arq.avello-tmp" cat "$arq.avello-tmp" > "$arq" rm -f "$arq.avello-tmp" anotar_escrito "$arq" ok "linhas ANTHROPIC_* removidas de $arq" done <<< "$lista" } gravar_claude() { mkdir -p "$HOME/.claude" anotar_e_copiar "$SETTINGS_CLAUDE" AVELLO_CHAVE_TEMP="$CHAVE" /usr/bin/osascript -l JavaScript - "$SETTINGS_CLAUDE" "$ORIGEM" \ "$MODELO_CLAUDE" "$MODELO_CLAUDE_OPUS" "$MODELO_CLAUDE_HAIKU" >/dev/null 2>&1 <<'JS' \ || falha "não consegui gravar $SETTINGS_CLAUDE (JSON inválido?). Ele não foi alterado." function run(argv) { ObjC.import("Foundation"); const caminho = argv[0]; const chave = ObjC.unwrap($.NSProcessInfo.processInfo.environment.objectForKey("AVELLO_CHAVE_TEMP")); let dados = {}; if ($.NSFileManager.defaultManager.fileExistsAtPath(caminho)) { const texto = ObjC.unwrap( $.NSString.stringWithContentsOfFileEncodingError(caminho, $.NSUTF8StringEncoding, null) ); if (typeof texto !== "string") throw new Error("ilegível"); if (texto.trim() !== "") dados = JSON.parse(texto); } if (typeof dados !== "object" || dados === null || Array.isArray(dados)) throw new Error("não é objeto"); const env = dados.env && typeof dados.env === "object" && !Array.isArray(dados.env) ? dados.env : {}; delete env.ANTHROPIC_API_KEY; env.ANTHROPIC_BASE_URL = argv[1]; env.ANTHROPIC_AUTH_TOKEN = chave; env.ANTHROPIC_MODEL = argv[2]; env.ANTHROPIC_DEFAULT_SONNET_MODEL = argv[2]; env.ANTHROPIC_DEFAULT_OPUS_MODEL = argv[3]; env.ANTHROPIC_DEFAULT_HAIKU_MODEL = argv[4]; dados.env = env; const gravou = $(JSON.stringify(dados, null, 2) + "\n").writeToFileAtomicallyEncodingError( caminho, true, $.NSUTF8StringEncoding, null ); if (!gravou) throw new Error("não gravou"); return "ok"; } JS chmod 600 "$SETTINGS_CLAUDE" anotar_escrito "$SETTINGS_CLAUDE" ok "Claude Code apontado para a Avello ($SETTINGS_CLAUDE)" } # Tira os blocos da Avello, uma tabela [model_providers.avello] feita à mão e o model/model_provider de topo. sem_avello_no_toml() { awk ' /^# >>> avello/ { bloco = 1; next } /^# <<< avello/ { bloco = 0; next } bloco { next } /^[ \t]*\[/ { tabela = 1 pular = ($0 ~ /^[ \t]*\[model_providers\.avello\][ \t]*(#.*)?$/) } pular { next } !tabela && /^[ \t]*(model|model_provider)[ \t]*=/ { next } { print } ' "$1" } gravar_codex() { local novo mkdir -p "$HOME/.codex" anotar_e_copiar "$CONFIG_CODEX" novo="$(mktemp)" { printf '%s\n' "# >>> avello (instalador Avello; para desfazer, rode o instalador e escolha Desfazer)" \ "model = \"$MODELO_CODEX\"" 'model_provider = "avello"' "# <<< avello" [ -f "$CONFIG_CODEX" ] && sem_avello_no_toml "$CONFIG_CODEX" printf '%s\n' "# >>> avello" "[model_providers.avello]" 'name = "Avello"' "base_url = \"$ORIGEM/v1\"" \ 'env_key = "AVELLO_API_KEY"' 'requires_openai_auth = false' 'wire_api = "responses"' "# <<< avello" } > "$novo" cat "$novo" > "$CONFIG_CODEX" rm -f "$novo" anotar_escrito "$CONFIG_CODEX" ok "Codex apontado para a Avello ($CONFIG_CODEX)" anotar_e_copiar "$ARQUIVO_DA_CHAVE" ( umask 077; printf '%s\n' "$CHAVE" > "$ARQUIVO_DA_CHAVE" ) ok "chave do Codex guardada em $ARQUIVO_DA_CHAVE (só você lê)" } # Fora do bloco da Avello, algum perfil do shell já põe ~/.local/bin no PATH (o instalador oficial, ou o cliente)? local_bin_no_perfil() { local arq for arq in "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.zshenv" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do [ -f "$arq" ] || continue awk '/^# >>> avello/{b=1;next} /^# <<< avello/{b=0;next} !b{print}' "$arq" | grep -Fq ".local/bin" && return 0 done return 1 } # O bloco da Avello no perfil do shell: a chave do Codex e, se nós instalamos em ~/.local/bin e nada garante esse # caminho num terminal novo, o PATH. Reinstalar troca o bloco; o Desfazer devolve o arquivo. gravar_bloco_do_shell() { local rc novo linhas="" [ "$FERRAMENTA" = claude ] || linhas="$LINHA_DA_CHAVE" if [ "$INSTALOU_EM_LOCAL_BIN" -eq 1 ]; then case ":$PATH_DE_QUEM_CHAMOU:" in *":$DIR_LOCAL_BIN:"*) ;; *) local_bin_no_perfil || linhas="$LINHA_DO_PATH${linhas:+$'\n'}$linhas" ;; esac fi [ -n "$linhas" ] || return 0 case "$(basename "${SHELL:-/bin/zsh}")" in bash) rc="$HOME/.bash_profile" ;; *) rc="$HOME/.zshrc" ;; esac anotar_e_copiar "$rc" novo="$(mktemp)" [ -f "$rc" ] && awk '/^# >>> avello/{b=1;next} /^# <<< avello/{b=0;next} !b{print}' "$rc" > "$novo" printf '%s\n' "# >>> avello" "$linhas" "# <<< avello" >> "$novo" cat "$novo" > "$rc" rm -f "$novo" anotar_escrito "$rc" } main() { ler_opcoes "$@" diga "" diga "Instalador Avello: Claude Code e Codex" diga "" [ "$(uname -s)" = "Darwin" ] || falha "este instalador é para Mac. No Windows, use o comando da aba Windows em $ORIGEM/comecar" escolher [ "$DESFAZER" -eq 1 ] && desfazer case "$FERRAMENTA" in claude|codex|ambos) ;; *) falha "ferramenta inválida: $FERRAMENTA (use claude, codex ou ambos)" ;; esac [ "$FERRAMENTA" = codex ] || garantir_claude [ "$FERRAMENTA" = claude ] || garantir_codex obter_chave if [ "$FERRAMENTA" != codex ]; then limpar_anthropic_do_shell gravar_claude fi [ "$FERRAMENTA" = claude ] || gravar_codex gravar_bloco_do_shell diga "" diga "Pronto. Abra um terminal NOVO e rode:" [ "$FERRAMENTA" = codex ] || diga " claude (modelo padrão: $MODELO_CLAUDE)" [ "$FERRAMENTA" = claude ] || diga " codex (modelo padrão: $MODELO_CODEX)" diga "" diga "Para desfazer: rode o mesmo comando e escolha 4." diga "Ids dos modelos e preços: $ORIGEM/comecar" diga "" } main "$@"