install.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -euo pipefail
# ── CianaParrot Installer ────────────────────────────────────────────────────
# Usage:
# curl -fsSL https://raw.githubusercontent.com/emanueleielo/ciana-parrot/main/install.sh | bash
# bash install.sh [--dry-run] [--no-prompt] [--help]
REPO_URL="https://github.com/emanueleielo/ciana-parrot.git"
INSTALL_DIR="${INSTALL_DIR:-$HOME/ciana-parrot}"
GATEWAY_PORT=9842
SERVICE_NAME="dev.cianaparrot.gateway"
# ── Flags ─────────────────────────────────────────────────────────────────────
DRY_RUN=false
NO_PROMPT=false
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--no-prompt) NO_PROMPT=true ;;
-h|--help)
echo "Usage: bash install.sh [--dry-run] [--no-prompt] [--help]"
echo ""
echo " --dry-run Preview actions without making changes"
echo " --no-prompt Non-interactive mode (reads from env vars)"
echo " --help Show this help message"
exit 0
;;
*) ;;
esac
done
# ── Helpers ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
info() { printf "${GREEN}[+]${RESET} %s\n" "$1"; }
warn() { printf "${YELLOW}[!]${RESET} %s\n" "$1"; }
error() { printf "${RED}[x]${RESET} %s\n" "$1" >&2; exit 1; }
step() { printf "\n${CYAN}${BOLD}── %s${RESET}\n" "$1"; }
# ── stdin workaround for curl | bash ─────────────────────────────────────────
# When piped, stdin is the script itself — redirect to the real terminal.
if [ "$NO_PROMPT" = false ] && [ "$DRY_RUN" = false ] && [ ! -t 0 ]; then
if [ -e /dev/tty ]; then
exec </dev/tty
else
NO_PROMPT=true
warn "No terminal available — switching to --no-prompt mode."
fi
fi
run() {
if [ "$DRY_RUN" = true ]; then
printf "${YELLOW}[dry-run]${RESET} "
printf "%q " "$@"
printf "\n"
else
"$@"
fi
}
prompt_value() {
local label="$1" var="$2" required="${3:-false}" secret="${4:-false}"
local value
# In no-prompt mode, read from environment
if [ "$NO_PROMPT" = true ]; then
value="${!var:-}"
if [ -z "$value" ] && [ "$required" = true ]; then
error "Missing required env var: $var"
fi
echo "$value"
return
fi
if [ "$required" = true ]; then
while true; do
printf " %s: " "$label" >&2
if [ "$secret" = true ]; then
read -r -s value
printf "\n" >&2
else
read -r value
fi
if [ -n "$value" ]; then break; fi
warn "This field is required."
done
else
printf " %s (Enter to skip): " "$label" >&2
if [ "$secret" = true ]; then
read -r -s value
printf "\n" >&2
else
read -r value
fi
fi
echo "$value"
}
generate_token() {
# Generate a random token using available tools
if command -v openssl &>/dev/null; then
openssl rand -hex 16
else
head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n' | head -c 32
fi
}
os_type() {
case "$(uname -s)" in
Darwin) echo "macos" ;;
Linux) echo "linux" ;;
*) echo "unknown" ;;
esac
}
# ── 1. Check prerequisites ───────────────────────────────────────────────────
step "Checking prerequisites"
HAS_ERRORS=false
# Docker
if ! command -v docker &>/dev/null; then
warn "Docker not found."
if [ "$(os_type)" = "macos" ]; then
warn " Install: https://docs.docker.com/desktop/install/mac-install/"
else
warn " Install: https://docs.docker.com/engine/install/"
fi
HAS_ERRORS=true
elif ! docker ps &>/dev/null; then
warn "Docker is installed but the daemon is not running."
if [ "$(os_type)" = "macos" ]; then
warn " Open Docker Desktop and try again."
else
warn " Run: sudo systemctl start docker"
warn " To avoid sudo: sudo usermod -aG docker \$USER && newgrp docker"
fi
HAS_ERRORS=true
else
info "Docker: $(docker --version | head -1)"
fi
# Docker Compose — detect plugin vs standalone
COMPOSE_CMD=""
if docker compose version &>/dev/null; then
COMPOSE_CMD="docker compose"
info "Docker Compose: $(docker compose version --short 2>/dev/null || echo 'ok')"
elif command -v docker-compose &>/dev/null; then
COMPOSE_CMD="docker-compose"
info "Docker Compose (standalone): $(docker-compose --version)"
else
warn "Docker Compose not found."
warn " It should be included with Docker Desktop."
warn " Or install: https://docs.docker.com/compose/install/"
HAS_ERRORS=true
fi
# Git
if command -v git &>/dev/null; then
info "Git: $(git --version)"
else
warn "Git not found."
if [ "$(os_type)" = "macos" ]; then
warn " Install: xcode-select --install"
else
warn " Install: sudo apt install git"
fi
HAS_ERRORS=true
fi
# Make
if command -v make &>/dev/null; then
info "Make: available"
else
warn "Make not found."
if [ "$(os_type)" = "macos" ]; then
warn " Install: xcode-select --install"
else
warn " Install: sudo apt install build-essential"
fi
HAS_ERRORS=true
fi
# Python3 (needed for host gateway)
if command -v python3 &>/dev/null; then
info "Python3: $(python3 --version)"
else
warn "Python3 not found (needed for host gateway)."
if [ "$(os_type)" = "macos" ]; then
warn " Install: brew install python3"
else
warn " Install: sudo apt install python3 python3-venv"
fi
HAS_ERRORS=true
fi
if [ "$HAS_ERRORS" = true ]; then
if [ "$DRY_RUN" = true ]; then
warn "Some prerequisites are missing (continuing in dry-run mode)."
else
error "Fix the issues above and re-run the installer."
fi
fi
# ── 2. Clone or update repo ──────────────────────────────────────────────────
step "Setting up repository"
if [ ! -d "$INSTALL_DIR" ]; then
info "Cloning to $INSTALL_DIR..."
run git clone "$REPO_URL" "$INSTALL_DIR"
elif [ -d "$INSTALL_DIR/.git" ]; then
info "Repository exists at $INSTALL_DIR"
cd "$INSTALL_DIR"
if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
warn "Local changes detected — skipping git pull to preserve your work."
run git fetch origin
else
info "No local changes — pulling latest..."
run git pull origin main
fi
else
error "$INSTALL_DIR exists but is not a git repository."
fi
if [ -d "$INSTALL_DIR" ]; then
cd "$INSTALL_DIR"
else
[ "$DRY_RUN" = true ] && info "Would cd to $INSTALL_DIR (does not exist yet)"
fi
# ── 3. Setup .env ────────────────────────────────────────────────────────────
step "Configuring environment"
set_env_var() {
local key="$1" value="$2" file="$INSTALL_DIR/.env"
local tmpfile="${file}.tmp"
if [ "$DRY_RUN" = true ]; then
info "[dry-run] Would set ${key} in .env"
return
fi
if grep -q "^${key}=" "$file" 2>/dev/null; then
# Update existing — rewrite line without sed (safe for any value)
k="$key" v="$value" awk 'BEGIN{FS=OFS="="} $1==ENVIRON["k"]{print ENVIRON["k"]"="ENVIRON["v"]; next} {print}' "$file" > "$tmpfile"
chmod 600 "$tmpfile"
mv "$tmpfile" "$file"
elif grep -q "^# *${key}=" "$file" 2>/dev/null; then
# Uncomment and set
k="$key" v="$value" awk '{if($0 ~ "^# *"ENVIRON["k"]"="){print ENVIRON["k"]"="ENVIRON["v"]}else{print}}' "$file" > "$tmpfile"
chmod 600 "$tmpfile"
mv "$tmpfile" "$file"
else
echo "${key}=${value}" >> "$file"
fi
chmod 600 "$file"
}
env_var_is_set() {
# Check if a key exists in .env with a real value (not empty, not a placeholder)
local key="$1"
local val
val=$(grep "^${key}=" "$INSTALL_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2-)
# Empty
[ -z "$val" ] && return 1
# Common placeholder patterns
[[ "$val" == *"..."* ]] && return 1
[[ "$val" =~ ^YOUR_ ]] && return 1
[[ "$val" =~ ^CHANGEME ]] && return 1
[[ "$val" =~ ^\<.*\>$ ]] && return 1
[[ "$val" =~ ^xxx+$ ]] && return 1
return 0
}
if [ -f "$INSTALL_DIR/.env" ]; then
info ".env already exists — checking for missing variables..."
# Add any new variables from .env.example that aren't in .env
UPDATED=false
while IFS= read -r line; do
# Skip comments and empty lines
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
key="${line%%=*}"
if ! grep -q "^${key}=" "$INSTALL_DIR/.env" 2>/dev/null; then
if [ "$DRY_RUN" = true ]; then
info "[dry-run] Would add missing variable: $key"
else
echo "$line" >> "$INSTALL_DIR/.env"
chmod 600 "$INSTALL_DIR/.env"
info "Added missing variable: $key"
fi
UPDATED=true
fi
done < "$INSTALL_DIR/.env.example"
if [ "$UPDATED" = false ]; then
info ".env is up to date."
fi
# Check required vars have real values
REQUIRED_VARS=("TELEGRAM_BOT_TOKEN" "ANTHROPIC_API_KEY")
for var in "${REQUIRED_VARS[@]}"; do
if ! env_var_is_set "$var"; then
if [ "$NO_PROMPT" = true ]; then
error "$var is missing or has a placeholder value in .env"
elif [ "$DRY_RUN" = false ]; then
warn "$var is missing or has a placeholder value."
NEW_VAL=$(prompt_value "$var" "$var" true true)
set_env_var "$var" "$NEW_VAL"
fi
fi
done
# Ensure gateway tokens exist
if ! grep -q "^GATEWAY_TOKEN=" "$INSTALL_DIR/.env" 2>/dev/null || ! env_var_is_set "GATEWAY_TOKEN"; then
GW_TOKEN="ciana-gw-$(generate_token)"
set_env_var "GATEWAY_TOKEN" "$GW_TOKEN"
set_env_var "CC_BRIDGE_TOKEN" "$GW_TOKEN"
info "Gateway token generated automatically."
fi
else
info "Creating .env from template..."
run cp "$INSTALL_DIR/.env.example" "$INSTALL_DIR/.env"
run chmod 600 "$INSTALL_DIR/.env"
if [ "$DRY_RUN" = true ]; then
info "Would prompt for API keys (skipping in dry-run)"
elif [ "$NO_PROMPT" = true ]; then
# Read from environment variables
TELEGRAM_TOKEN=$(prompt_value "Telegram Bot Token" TELEGRAM_BOT_TOKEN true true)
set_env_var "TELEGRAM_BOT_TOKEN" "$TELEGRAM_TOKEN"
ANTHROPIC_KEY=$(prompt_value "Anthropic API Key" ANTHROPIC_API_KEY true true)
set_env_var "ANTHROPIC_API_KEY" "$ANTHROPIC_KEY"
GW_TOKEN="ciana-gw-$(generate_token)"
set_env_var "GATEWAY_TOKEN" "$GW_TOKEN"
set_env_var "CC_BRIDGE_TOKEN" "$GW_TOKEN"
# Optional — set only if present in env
for var in OPENAI_API_KEY BRAVE_API_KEY GH_TOKEN GEMINI_API_KEY ELEVENLABS_API_KEY; do
[ -n "${!var:-}" ] && set_env_var "$var" "${!var}"
done
info ".env configured from environment variables."
else
printf "\n Enter your API keys below. Secrets are never displayed.\n"
printf "\n ${BOLD}Telegram Bot:${RESET} Open Telegram, search @BotFather, send /newbot and follow the steps.\n"
printf " Copy the token it gives you and paste it below.\n\n"
# Required (secret=true hides input)
TELEGRAM_TOKEN=$(prompt_value "Telegram Bot Token" TELEGRAM_BOT_TOKEN true true)
set_env_var "TELEGRAM_BOT_TOKEN" "$TELEGRAM_TOKEN"
ANTHROPIC_KEY=$(prompt_value "Anthropic API Key" ANTHROPIC_API_KEY true true)
set_env_var "ANTHROPIC_API_KEY" "$ANTHROPIC_KEY"
# Auto-generate gateway token
GW_TOKEN="ciana-gw-$(generate_token)"
set_env_var "GATEWAY_TOKEN" "$GW_TOKEN"
set_env_var "CC_BRIDGE_TOKEN" "$GW_TOKEN"
info "Gateway token generated automatically."
# Optional (all secrets)
printf "\n Optional keys (press Enter to skip):\n\n"
OPENAI_KEY=$(prompt_value "OpenAI API Key (voice transcription + image gen)" OPENAI_API_KEY false true)
[ -n "$OPENAI_KEY" ] && set_env_var "OPENAI_API_KEY" "$OPENAI_KEY"
BRAVE_KEY=$(prompt_value "Brave Search API Key (or uses DuckDuckGo)" BRAVE_API_KEY false true)
[ -n "$BRAVE_KEY" ] && set_env_var "BRAVE_API_KEY" "$BRAVE_KEY"
GH_KEY=$(prompt_value "GitHub Token" GH_TOKEN false true)
[ -n "$GH_KEY" ] && set_env_var "GH_TOKEN" "$GH_KEY"
GEMINI_KEY=$(prompt_value "Gemini API Key (image gen + PDF)" GEMINI_API_KEY false true)
[ -n "$GEMINI_KEY" ] && set_env_var "GEMINI_API_KEY" "$GEMINI_KEY"
ELEVENLABS_KEY=$(prompt_value "ElevenLabs API Key (text-to-speech)" ELEVENLABS_API_KEY false true)
[ -n "$ELEVENLABS_KEY" ] && set_env_var "ELEVENLABS_API_KEY" "$ELEVENLABS_KEY"
info ".env configured."
fi
fi
# ── 4. Build and start Docker ────────────────────────────────────────────────
step "Building and starting CianaParrot"
if [ -n "$COMPOSE_CMD" ]; then
run $COMPOSE_CMD build
run $COMPOSE_CMD up -d
else
run make build
run make up
fi
if [ "$DRY_RUN" = false ] && [ -n "$COMPOSE_CMD" ]; then
sleep 3
if $COMPOSE_CMD ps --format json 2>/dev/null | grep -q '"running"'; then
info "Docker container is running."
elif $COMPOSE_CMD ps 2>/dev/null | grep -q "Up"; then
info "Docker container is running."
else
warn "Container may not be running. Check: make logs"
fi
fi
# ── 5. Setup gateway ─────────────────────────────────────────────────────────
step "Setting up host gateway"
# Check if port is in use (with fallback for systems without lsof)
port_in_use() {
if command -v lsof &>/dev/null; then
lsof -i ":$GATEWAY_PORT" &>/dev/null
elif command -v ss &>/dev/null; then
ss -ltn "sport = :$GATEWAY_PORT" 2>/dev/null | grep -q LISTEN
elif curl -sf "http://localhost:$GATEWAY_PORT/health" &>/dev/null; then
return 0
else
return 1
fi
}
if port_in_use; then
EXISTING_PID=""
if command -v lsof &>/dev/null; then
EXISTING_PID=$(lsof -t -i ":$GATEWAY_PORT" 2>/dev/null | head -1 || true)
fi
if [ -n "$EXISTING_PID" ]; then
warn "Port $GATEWAY_PORT is already in use (PID: $EXISTING_PID)."
else
warn "Port $GATEWAY_PORT is already in use."
fi
warn "If that's a previous gateway, it's already running — skipping."
else
# Setup venv for gateway
if [ ! -d "$INSTALL_DIR/.venv" ]; then
info "Creating Python venv for gateway..."
run python3 -m venv "$INSTALL_DIR/.venv"
fi
# Install/update gateway dependencies (always, so re-runs pick up changes)
if [ -f "$INSTALL_DIR/src/gateway/requirements.txt" ]; then
run "$INSTALL_DIR/.venv/bin/pip" install -q --upgrade -r "$INSTALL_DIR/src/gateway/requirements.txt"
else
run "$INSTALL_DIR/.venv/bin/pip" install -q --upgrade pyyaml "pydantic>=2,<3" python-dotenv
fi
SETUP_SERVICE=false
if [ "$NO_PROMPT" = false ] && [ "$DRY_RUN" = false ]; then
printf "\n Start gateway automatically on boot? [Y/n] "
read -r ANSWER
ANSWER="${ANSWER:-Y}"
if [[ "$ANSWER" =~ ^[Yy] ]]; then
SETUP_SERVICE=true
fi
fi
if [ "$SETUP_SERVICE" = true ] && [ "$DRY_RUN" = false ]; then
OS=$(os_type)
if [ "$OS" = "macos" ]; then
# launchd user agent
PLIST_DIR="$HOME/Library/LaunchAgents"
PLIST_FILE="$PLIST_DIR/${SERVICE_NAME}.plist"
LOG_DIR="$INSTALL_DIR/logs"
mkdir -p "$PLIST_DIR" "$LOG_DIR"
cat > "$PLIST_FILE" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>${SERVICE_NAME}</string>
<key>ProgramArguments</key>
<array>
<string>${INSTALL_DIR}/.venv/bin/python</string>
<string>${INSTALL_DIR}/src/gateway/server.py</string>
</array>
<key>WorkingDirectory</key>
<string>${INSTALL_DIR}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>${LOG_DIR}/gateway.log</string>
<key>StandardErrorPath</key>
<string>${LOG_DIR}/gateway.log</string>
</dict>
</plist>
PLIST
# Unload first if already loaded (idempotent)
launchctl bootout "gui/$(id -u)/$SERVICE_NAME" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$PLIST_FILE"
info "Gateway registered as launchd service (starts on boot)."
info " Logs: $LOG_DIR/gateway.log"
info " Stop: launchctl bootout gui/$(id -u)/$SERVICE_NAME"
elif [ "$OS" = "linux" ]; then
# systemd user service
UNIT_DIR="$HOME/.config/systemd/user"
mkdir -p "$UNIT_DIR"
UNIT_FILE="$UNIT_DIR/cianaparrot-gateway.service"
cat > "$UNIT_FILE" <<UNIT
[Unit]
Description=CianaParrot Host Gateway
After=network.target
[Service]
Type=simple
WorkingDirectory=${INSTALL_DIR}
ExecStart=${INSTALL_DIR}/.venv/bin/python ${INSTALL_DIR}/src/gateway/server.py
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
UNIT
systemctl --user daemon-reload
systemctl --user enable --now cianaparrot-gateway.service
info "Gateway registered as systemd user service (starts on boot)."
info " Logs: journalctl --user -u cianaparrot-gateway -f"
info " Stop: systemctl --user stop cianaparrot-gateway"
fi
else
# Background process — clean stale pidfile
if [ -f "$INSTALL_DIR/.gateway.pid" ]; then
OLD_PID=$(cat "$INSTALL_DIR/.gateway.pid" 2>/dev/null)
if [ -n "$OLD_PID" ] && ! kill -0 "$OLD_PID" 2>/dev/null; then
rm -f "$INSTALL_DIR/.gateway.pid"
fi
fi
info "Starting gateway in background..."
if [ "$DRY_RUN" = false ]; then
mkdir -p "$INSTALL_DIR/logs"
nohup "$INSTALL_DIR/.venv/bin/python" "$INSTALL_DIR/src/gateway/server.py" \
> "$INSTALL_DIR/logs/gateway.log" 2>&1 &
GW_PID=$!
echo "$GW_PID" > "$INSTALL_DIR/.gateway.pid"
info "Gateway started (PID: $GW_PID)."
info " Logs: tail -f $INSTALL_DIR/logs/gateway.log"
info " Stop: kill \$(cat $INSTALL_DIR/.gateway.pid)"
else
run nohup ".venv/bin/python" src/gateway/server.py
fi
fi
# Verify gateway is running
if [ "$DRY_RUN" = false ]; then
sleep 2
if curl -sf "http://localhost:$GATEWAY_PORT/health" &>/dev/null; then
info "Gateway is healthy on port $GATEWAY_PORT."
else
warn "Gateway may not be ready yet. Check: tail -f $INSTALL_DIR/logs/gateway.log"
fi
fi
fi
# ── Done ──────────────────────────────────────────────────────────────────────
step "CianaParrot is ready!"
printf "
${GREEN}${BOLD}Open Telegram, find your bot, and send /start${RESET}
Useful commands (run from ${INSTALL_DIR}):
${CYAN}make logs${RESET} Follow bot logs
${CYAN}make down${RESET} Stop the bot
${CYAN}make restart${RESET} Rebuild and restart
${CYAN}make gateway${RESET} Start gateway manually (if not using a service)
Re-run installer with flags:
${CYAN}bash install.sh --dry-run${RESET} Preview without changes
${CYAN}bash install.sh --no-prompt${RESET} Non-interactive (reads env vars)
Or via curl:
${CYAN}curl -fsSL .../install.sh | bash -s -- --dry-run${RESET}
${CYAN}curl -fsSL .../install.sh | bash -s -- --no-prompt${RESET}
"