#!/usr/bin/env bash set -euo pipefail BINARY_NAME="email-mcp" DEFAULT_PROFILE="default" PROFILE_ENV="EMAIL_MCP_PROFILE" RELEASE_BASE_URL="https://forge.lclr.dev" RELEASE_REPOSITORY="AI/email-mcp" INSTALLED_BINARY_PATH="" PREFILL_SERVER_NAME="" PREFILL_PROFILE_VALUE="" PREFILL_COMMAND_PATH="" if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then C_RESET="$(printf '\033[0m')" C_BOLD="$(printf '\033[1m')" C_DIM="$(printf '\033[2m')" C_RED="$(printf '\033[31m')" C_GREEN="$(printf '\033[32m')" C_YELLOW="$(printf '\033[33m')" C_BLUE="$(printf '\033[34m')" C_MAGENTA="$(printf '\033[35m')" C_CYAN="$(printf '\033[36m')" else C_RESET="" C_BOLD="" C_DIM="" C_RED="" C_GREEN="" C_YELLOW="" C_BLUE="" C_MAGENTA="" C_CYAN="" fi ui_line() { printf "%b%s%b\n" "$C_DIM" "------------------------------------------------------------" "$C_RESET" >&2 } ui_title() { printf "\n%b%s%b\n" "$C_BOLD$C_CYAN" "$1" "$C_RESET" >&2 } ui_info() { printf "%b[info]%b %s\n" "$C_BLUE" "$C_RESET" "$1" >&2 } ui_success() { printf "%b[ok]%b %s\n" "$C_GREEN" "$C_RESET" "$1" >&2 } ui_warn() { printf "%b[warn]%b %s\n" "$C_YELLOW" "$C_RESET" "$1" >&2 } ui_error() { printf "%b[error]%b %s\n" "$C_RED" "$C_RESET" "$1" >&2 } prompt() { local label="$1" local default_value="$2" local answer="" if [ -n "$default_value" ]; then printf "%b%s%b [%s]: " "$C_BOLD" "$label" "$C_RESET" "$default_value" >&2 else printf "%b%s%b: " "$C_BOLD" "$label" "$C_RESET" >&2 fi if [ -t 2 ] && [ -r /dev/tty ]; then if ! IFS= read -r answer < /dev/tty 2>/dev/null; then IFS= read -r answer || answer="" fi else IFS= read -r answer || answer="" fi if [ -z "$answer" ]; then printf "%s" "$default_value" return fi printf "%s" "$answer" } tty_prompt_available() { [ -t 2 ] && [ -r /dev/tty ] && [ -w /dev/tty ] } menu_select() { local title="$1" shift local options=("$@") local count="${#options[@]}" local index=0 local key="" local i=0 local rows=$((count + 3)) local rendered=0 if [ "$count" -eq 0 ]; then return 1 fi if ! tty_prompt_available; then ui_title "$title" i=0 while [ "$i" -lt "$count" ]; do printf " %d) %s\n" "$((i + 1))" "${options[$i]}" >&2 i=$((i + 1)) done while true; do local raw_choice raw_choice="$(prompt "Choix" "1")" case "$raw_choice" in ''|*[!0-9]*) ui_warn "Choix invalide: $raw_choice" ;; *) if [ "$raw_choice" -ge 1 ] && [ "$raw_choice" -le "$count" ]; then printf "%s" "${options[$((raw_choice - 1))]}" return 0 fi ui_warn "Choix invalide: $raw_choice" ;; esac done fi while true; do if [ "$rendered" -eq 1 ]; then printf "\033[%dA\033[J" "$rows" >&2 2>/dev/null || true fi ui_title "$title" i=0 while [ "$i" -lt "$count" ]; do if [ "$i" -eq "$index" ]; then printf " %b› %s%b\n" "$C_BOLD$C_CYAN" "${options[$i]}" "$C_RESET" >&2 else printf " %s\n" "${options[$i]}" >&2 fi i=$((i + 1)) done printf "%bUtilise ↑/↓ puis Entrée.%b\n" "$C_DIM" "$C_RESET" >&2 rendered=1 if ! IFS= read -rsn1 key < /dev/tty; then continue fi case "$key" in "") printf "%s" "${options[$index]}" return 0 ;; $'\x1b') if IFS= read -rsn2 key < /dev/tty; then case "$key" in "[A") if [ "$index" -eq 0 ]; then index=$((count - 1)) else index=$((index - 1)) fi ;; "[B") if [ "$index" -eq $((count - 1)) ]; then index=0 else index=$((index + 1)) fi ;; esac fi ;; esac done } sanitize_server_name() { local raw="$1" local sanitized sanitized="$(printf "%s" "$raw" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_-]/-/g; s/--*/-/g; s/^-*//; s/-*$//')" if [ -z "$sanitized" ]; then sanitized="$BINARY_NAME" fi printf "%s" "$sanitized" } toml_escape() { printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' } go_bin_dir() { local gobin gobin="$(go env GOBIN 2>/dev/null || true)" if [ -n "$gobin" ]; then printf "%s\n" "$gobin" return fi go env GOPATH 2>/dev/null | awk '{print $1 "/bin"}' } resolve_binary_path() { if [ -n "$INSTALLED_BINARY_PATH" ] && [ -x "$INSTALLED_BINARY_PATH" ]; then printf "%s\n" "$INSTALLED_BINARY_PATH" return fi if command -v "$BINARY_NAME" >/dev/null 2>&1; then command -v "$BINARY_NAME" return fi if command -v go >/dev/null 2>&1; then local bin_dir bin_dir="$(go_bin_dir)" if [ -n "$bin_dir" ] && [ -x "$bin_dir/$BINARY_NAME" ]; then printf "%s\n" "$bin_dir/$BINARY_NAME" return fi fi printf "%s\n" "$HOME/.local/bin/$BINARY_NAME" } ensure_cli() { local cli_name="$1" if command -v "$cli_name" >/dev/null 2>&1; then return fi ui_error "Commande introuvable: $cli_name" exit 1 } detect_os() { case "$(uname -s | tr '[:upper:]' '[:lower:]')" in linux) printf "linux" ;; darwin) printf "darwin" ;; *) ui_error "OS non supporte: $(uname -s)" exit 1 ;; esac } detect_arch() { case "$(uname -m)" in x86_64|amd64) printf "amd64" ;; aarch64|arm64) printf "arm64" ;; *) ui_error "Architecture non supportee: $(uname -m)" exit 1 ;; esac } release_api_url() { printf "%s/api/v1/repos/%s/releases/latest\n" "${RELEASE_BASE_URL%/}" "$RELEASE_REPOSITORY" } extract_asset_url() { local json="$1" local asset_name="$2" printf "%s\n" "$json" \ | grep -o '"browser_download_url":"[^"]*"' \ | sed 's/"browser_download_url":"//;s/"$//' \ | sed 's#\\/#/#g' \ | grep "/${asset_name}$" \ | head -n 1 } verify_checksum() { local binary_path="$1" local checksum_path="$2" if command -v sha256sum >/dev/null 2>&1; then ( cd "$(dirname "$binary_path")" && sha256sum -c "$(basename "$checksum_path")" ) return fi if command -v shasum >/dev/null 2>&1; then local expected local actual expected="$(awk '{print $1}' "$checksum_path")" actual="$(shasum -a 256 "$binary_path" | awk '{print $1}')" if [ "$expected" != "$actual" ]; then ui_error "Checksum invalide pour $binary_path" exit 1 fi return fi ui_warn "Aucun utilitaire checksum trouve (sha256sum/shasum), verification ignoree." } download_latest_release_binary() { if ! command -v curl >/dev/null 2>&1; then ui_error "curl est requis pour telecharger la release." exit 1 fi local os_name local arch_name local ext="" os_name="$(detect_os)" arch_name="$(detect_arch)" if [ "$os_name" = "windows" ]; then ext=".exe" fi local asset_name local checksum_name asset_name="${BINARY_NAME}-${os_name}-${arch_name}${ext}" checksum_name="${asset_name}.sha256" ui_info "Recuperation de la derniere release..." local release_json release_json="$(curl -fsSL "$(release_api_url)")" local asset_url asset_url="$(extract_asset_url "$release_json" "$asset_name")" if [ -z "$asset_url" ]; then ui_error "Asset introuvable dans la derniere release: $asset_name" exit 1 fi local checksum_url checksum_url="$(extract_asset_url "$release_json" "$checksum_name" || true)" local tmp_dir tmp_dir="$(mktemp -d)" trap 'if [ -n "${tmp_dir:-}" ] && [ -d "${tmp_dir:-}" ]; then rm -rf "${tmp_dir}"; fi' EXIT INT TERM curl -fsSL "$asset_url" -o "$tmp_dir/$asset_name" chmod +x "$tmp_dir/$asset_name" if [ -n "$checksum_url" ]; then curl -fsSL "$checksum_url" -o "$tmp_dir/$checksum_name" verify_checksum "$tmp_dir/$asset_name" "$tmp_dir/$checksum_name" fi local target_dir target_dir="$(prompt "Repertoire d'installation" "$HOME/.local/bin")" mkdir -p "$target_dir" install -m 0755 "$tmp_dir/$asset_name" "$target_dir/$BINARY_NAME" INSTALLED_BINARY_PATH="$target_dir/$BINARY_NAME" ui_success "Binaire installe: $INSTALLED_BINARY_PATH" ui_info "Ajoute ce dossier au PATH si necessaire." } install_binary() { if command -v "$BINARY_NAME" >/dev/null 2>&1; then INSTALLED_BINARY_PATH="$(command -v "$BINARY_NAME")" ui_success "Binaire detecte: $INSTALLED_BINARY_PATH" local reinstall reinstall="$(prompt "Forcer une reinstall depuis la derniere release ? (y/N)" "N")" case "$reinstall" in y|Y|yes|YES) download_latest_release_binary return ;; *) return ;; esac fi download_latest_release_binary } run_setup_wizard() { install_binary local profile profile="$(prompt "Profil a configurer (${PROFILE_ENV})" "$DEFAULT_PROFILE")" local binary_path binary_path="$(resolve_binary_path)" if [ ! -x "$binary_path" ]; then ui_error "Binaire introuvable/executable: $binary_path" exit 1 fi ui_info "Déverrouillage Bitwarden avant le setup..." if [ -t 2 ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then env "${PROFILE_ENV}=${profile}" "$binary_path" login < /dev/tty > /dev/tty else env "${PROFILE_ENV}=${profile}" "$binary_path" login fi ui_info "Lancement de $BINARY_NAME setup" if [ -t 2 ] && [ -r /dev/tty ] && [ -w /dev/tty ]; then env "${PROFILE_ENV}=${profile}" "$binary_path" setup < /dev/tty > /dev/tty else env "${PROFILE_ENV}=${profile}" "$binary_path" setup fi ui_success "Setup termine pour le profil \"$profile\"." PREFILL_SERVER_NAME="$(sanitize_server_name "$BINARY_NAME")" PREFILL_PROFILE_VALUE="$profile" PREFILL_COMMAND_PATH="$binary_path" } collect_server_inputs() { local default_name default_name="$(sanitize_server_name "${PREFILL_SERVER_NAME:-$BINARY_NAME}")" SERVER_NAME="$(prompt "Nom du serveur MCP" "$default_name")" SERVER_NAME="$(sanitize_server_name "$SERVER_NAME")" PROFILE_VALUE="$(prompt "Valeur de ${PROFILE_ENV}" "${PREFILL_PROFILE_VALUE:-$DEFAULT_PROFILE}")" local default_command if [ -n "${PREFILL_COMMAND_PATH:-}" ]; then default_command="$PREFILL_COMMAND_PATH" else default_command="$(resolve_binary_path)" fi COMMAND_PATH="$(prompt "Chemin du binaire serveur MCP" "$default_command")" } choose_scope() { local selected selected="$(menu_select "Scope de configuration" "global (user)" "project (projet courant)")" case "$selected" in "global (user)") printf "global" ;; *) printf "project" ;; esac } apply_claude_mcp() { ensure_cli "claude" collect_server_inputs local scope_choice scope_choice="$(choose_scope)" local claude_scope if [ "$scope_choice" = "global" ]; then claude_scope="user" else claude_scope="project" fi ui_info "Application de la configuration Claude ($claude_scope)..." claude mcp remove --scope "$claude_scope" "$SERVER_NAME" >/dev/null 2>&1 || true claude mcp add \ --transport stdio \ --scope "$claude_scope" \ "$SERVER_NAME" \ --env "${PROFILE_ENV}=${PROFILE_VALUE}" \ -- "$COMMAND_PATH" mcp ui_success "Serveur \"$SERVER_NAME\" configure dans Claude ($claude_scope)." } rewrite_codex_project_config() { local project_dir="$1" local config_file="$project_dir/.codex/config.toml" local section_prefix="[mcp_servers.${SERVER_NAME}" mkdir -p "$project_dir/.codex" touch "$config_file" local tmp_file tmp_file="$(mktemp)" awk -v prefix="$section_prefix" ' function is_target(line, p, next_char) { if (index(line, p) != 1) { return 0 } next_char = substr(line, length(p) + 1, 1) return next_char == "]" || next_char == "." } /^\[.*\]$/ { if (is_target($0, prefix)) { skip = 1 next } if (skip == 1) { skip = 0 } } { if (skip != 1) { print $0 } } ' "$config_file" > "$tmp_file" mv "$tmp_file" "$config_file" { printf "\n[mcp_servers.%s]\n" "$SERVER_NAME" printf "command = \"%s\"\n" "$(toml_escape "$COMMAND_PATH")" printf "args = [\"mcp\"]\n\n" printf "[mcp_servers.%s.env]\n" "$SERVER_NAME" printf "%s = \"%s\"\n" "$PROFILE_ENV" "$(toml_escape "$PROFILE_VALUE")" } >> "$config_file" } apply_codex_mcp() { ensure_cli "codex" collect_server_inputs local scope_choice scope_choice="$(choose_scope)" if [ "$scope_choice" = "global" ]; then ui_info "Application via codex mcp add (scope global)..." codex mcp remove "$SERVER_NAME" >/dev/null 2>&1 || true codex mcp add \ "$SERVER_NAME" \ --env "${PROFILE_ENV}=${PROFILE_VALUE}" \ -- "$COMMAND_PATH" mcp ui_success "Serveur \"$SERVER_NAME\" configure dans le scope global Codex." return fi local default_project_dir default_project_dir="$(pwd)" local project_dir project_dir="$(prompt "Dossier projet cible pour .codex/config.toml" "$default_project_dir")" rewrite_codex_project_config "$project_dir" ui_success "Configuration projet ecrite dans $project_dir/.codex/config.toml" } print_mcp_json() { collect_server_inputs cat <&2 printf "%bRelease repo:%b %s\n" "$C_DIM" "$C_RESET" "$RELEASE_REPOSITORY" >&2 ui_line printf "%bSelectionne une action dans le menu interactif.%b\n" "$C_DIM" "$C_RESET" >&2 } post_setup_configure_mcp() { ui_title "Configuration MCP apres setup" local next_action next_action="$(menu_select \ "Configurer le MCP maintenant ?" \ "Configurer Claude Code (apply direct)" \ "Configurer Codex (apply direct)" \ "Generer JSON MCP manuel" \ "Terminer sans config MCP")" printf "\n" >&2 case "$next_action" in "Configurer Claude Code (apply direct)") apply_claude_mcp ;; "Configurer Codex (apply direct)") apply_codex_mcp ;; "Generer JSON MCP manuel") ui_info "JSON MCP genere sur stdout." print_mcp_json ;; *) ui_info "Setup termine sans configuration MCP additionnelle." ;; esac } main() { print_header local action action="$(menu_select \ "Choisis une action" \ "Installer/mettre a jour le binaire + setup" \ "Configurer Claude Code (apply direct)" \ "Configurer Codex (apply direct)" \ "Generer JSON MCP manuel" \ "Quitter")" printf "\n" >&2 case "$action" in "Installer/mettre a jour le binaire + setup") run_setup_wizard post_setup_configure_mcp ;; "Configurer Claude Code (apply direct)") apply_claude_mcp ;; "Configurer Codex (apply direct)") apply_codex_mcp ;; "Generer JSON MCP manuel") ui_info "JSON MCP genere sur stdout." print_mcp_json ;; *) ui_warn "Annule." ;; esac } main "$@"