install.sh 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. # Colors for output
  4. RED='\033[0;31m'
  5. GREEN='\033[0;32m'
  6. YELLOW='\033[1;33m'
  7. BLUE='\033[0;34m'
  8. CYAN='\033[0;36m'
  9. MAGENTA='\033[0;35m'
  10. ORANGE='\033[38;2;255;140;0m'
  11. BOLD='\033[1m'
  12. DIM='\033[2m'
  13. NC='\033[0m' # No Color
  14. # Configuration
  15. INSTALL_DIR="${CLINE_INSTALL_DIR:-$HOME/.cline/cli}"
  16. GITHUB_REPO="cline/cline"
  17. requested_version="${CLINE_VERSION:-}"
  18. FORCE_INSTALL="${FORCE_INSTALL:-false}"
  19. # Detect OS and architecture
  20. os=$(uname -s | tr '[:upper:]' '[:lower:]')
  21. arch=$(uname -m)
  22. # Normalize architecture names
  23. if [[ "$arch" == "aarch64" ]]; then
  24. arch="arm64"
  25. elif [[ "$arch" == "x86_64" ]]; then
  26. arch="x64"
  27. fi
  28. # Determine platform string
  29. case "$os" in
  30. darwin)
  31. [[ "$arch" == "x64" || "$arch" == "arm64" ]] || {
  32. echo -e "${RED}${BOLD}ERROR${NC} ${RED}Unsupported architecture: $arch${NC}" >&2
  33. exit 1
  34. }
  35. platform="darwin-$arch"
  36. ;;
  37. linux)
  38. [[ "$arch" == "x64" || "$arch" == "arm64" ]] || {
  39. echo -e "${RED}${BOLD}ERROR${NC} ${RED}Unsupported architecture: $arch${NC}" >&2
  40. exit 1
  41. }
  42. platform="linux-$arch"
  43. ;;
  44. *)
  45. echo -e "${RED}${BOLD}ERROR${NC} ${RED}Unsupported OS: $os${NC}" >&2
  46. exit 1
  47. ;;
  48. esac
  49. # Print colored message
  50. print_message() {
  51. local color=$1
  52. shift
  53. echo -e "${color}$@${NC}"
  54. }
  55. # Print step
  56. print_step() {
  57. local message=$1
  58. echo -e "${CYAN}→${NC} ${DIM}$message${NC}"
  59. }
  60. # Print success
  61. print_ok() {
  62. local message=$1
  63. echo -e "${GREEN}✓${NC} $message"
  64. }
  65. # Print error
  66. print_error() {
  67. local message=$1
  68. echo -e "${RED}✗${NC} ${RED}$message${NC}" >&2
  69. }
  70. # Check prerequisites
  71. check_prerequisites() {
  72. print_step "Checking prerequisites"
  73. for cmd in curl tar; do
  74. if ! command -v "$cmd" >/dev/null 2>&1; then
  75. print_error "$cmd is required but not installed"
  76. exit 1
  77. fi
  78. done
  79. print_ok "Prerequisites satisfied"
  80. }
  81. # Check GitHub API rate limit status and return details
  82. check_rate_limit() {
  83. local rate_limit_response=$(curl -s "https://api.github.com/rate_limit" 2>/dev/null)
  84. if [ -z "$rate_limit_response" ]; then
  85. return 1 # Can't determine rate limit status
  86. fi
  87. if command -v jq >/dev/null 2>&1; then
  88. local remaining=$(echo "$rate_limit_response" | jq -r '.rate.remaining' 2>/dev/null)
  89. if [ "$remaining" = "0" ]; then
  90. return 0 # Rate limited
  91. fi
  92. fi
  93. return 1 # Not rate limited
  94. }
  95. # Show detailed rate limit error
  96. show_rate_limit_error() {
  97. local rate_limit_response=$(curl -s "https://api.github.com/rate_limit" 2>/dev/null)
  98. if command -v jq >/dev/null 2>&1; then
  99. local remaining=$(echo "$rate_limit_response" | jq -r '.rate.remaining' 2>/dev/null)
  100. local limit=$(echo "$rate_limit_response" | jq -r '.rate.limit' 2>/dev/null)
  101. local reset=$(echo "$rate_limit_response" | jq -r '.rate.reset' 2>/dev/null)
  102. local used=$(echo "$rate_limit_response" | jq -r '.rate.used' 2>/dev/null)
  103. print_error "GitHub API rate limit exceeded"
  104. echo ""
  105. echo -e "${YELLOW}Rate Limit Status:${NC}"
  106. echo -e " ${CYAN}Used:${NC} ${BOLD}$used${NC} / $limit requests"
  107. echo -e " ${CYAN}Remaining:${NC} ${RED}${BOLD}$remaining${NC}"
  108. echo -e " ${CYAN}Resets at:${NC} ${BOLD}$(date -r $reset 2>/dev/null || date -d @$reset 2>/dev/null)${NC}"
  109. echo ""
  110. # Calculate time until reset
  111. local now=$(date +%s)
  112. local seconds_until_reset=$((reset - now))
  113. local minutes_until_reset=$((seconds_until_reset / 60))
  114. if [ $seconds_until_reset -gt 0 ]; then
  115. echo -e "${YELLOW}Your rate limit will reset in ${BOLD}~$minutes_until_reset minutes${NC}"
  116. echo ""
  117. fi
  118. echo -e "${CYAN}Options:${NC}"
  119. echo -e " ${DIM}1.${NC} Wait for the rate limit to reset"
  120. echo -e " ${DIM}2.${NC} Use a GitHub Personal Access Token for 5,000 requests/hour:"
  121. echo ""
  122. echo -e " ${GREEN}GITHUB_TOKEN=your_token bash scripts/install.sh${NC}"
  123. echo ""
  124. echo -e " ${DIM}Create a token at: https://github.com/settings/tokens${NC}"
  125. echo ""
  126. else
  127. print_error "GitHub API rate limit exceeded"
  128. echo ""
  129. echo -e "${YELLOW}You've used all 60 requests. Please wait ~1 hour or use a GitHub token.${NC}"
  130. echo ""
  131. fi
  132. }
  133. # Get download URL and version
  134. get_release_info() {
  135. if [ -z "$requested_version" ]; then
  136. print_step "Fetching latest CLI release"
  137. # Build auth header if token provided
  138. local auth_header=""
  139. if [ -n "${GITHUB_TOKEN:-}" ]; then
  140. auth_header="-H \"Authorization: Bearer $GITHUB_TOKEN\""
  141. fi
  142. # Use jq if available for more reliable parsing
  143. if command -v jq >/dev/null 2>&1; then
  144. local response=$(eval curl -fsSL $auth_header "https://api.github.com/repos/$GITHUB_REPO/releases" 2>&1)
  145. local curl_exit=$?
  146. # If curl failed, diagnose why
  147. if [ $curl_exit -ne 0 ]; then
  148. # Check if it's a rate limit issue
  149. if check_rate_limit; then
  150. show_rate_limit_error
  151. exit 1
  152. fi
  153. # Check if it's a network issue
  154. if ! curl -s --connect-timeout 5 "https://api.github.com" >/dev/null 2>&1; then
  155. print_error "Could not connect to GitHub"
  156. echo ""
  157. echo -e "${YELLOW}Please check your internet connection and try again.${NC}"
  158. echo ""
  159. exit 1
  160. fi
  161. # Generic error
  162. print_error "Failed to fetch releases from GitHub"
  163. echo ""
  164. echo -e "${DIM}Error: $response${NC}"
  165. echo ""
  166. exit 1
  167. fi
  168. # Check if response is valid JSON
  169. if ! echo "$response" | jq empty 2>/dev/null; then
  170. print_error "Invalid response from GitHub API"
  171. echo ""
  172. echo -e "${DIM}Response preview:${NC}"
  173. echo "$response" | head -5
  174. echo ""
  175. # Double-check rate limit
  176. if check_rate_limit; then
  177. show_rate_limit_error
  178. fi
  179. exit 1
  180. fi
  181. # Parse release info
  182. local release_info=$(echo "$response" | \
  183. jq -r '.[] | select(.tag_name | endswith("-cli")) | .tag_name + "|" + (.assets[] | select(.name | contains("'"$platform"'") and endswith(".tar.gz")) | .browser_download_url) | select(length > 0)' 2>/dev/null | head -1)
  184. if [ -z "$release_info" ]; then
  185. # No matching release found - show what's available
  186. local latest_cli_tag=$(echo "$response" | jq -r '.[] | select(.tag_name | endswith("-cli")) | .tag_name' 2>/dev/null | head -1)
  187. if [ -z "$latest_cli_tag" ]; then
  188. print_error "No CLI releases found"
  189. echo ""
  190. echo -e "${DIM}Visit: https://github.com/$GITHUB_REPO/releases${NC}"
  191. echo ""
  192. exit 1
  193. fi
  194. print_error "No release found for platform: $platform"
  195. echo ""
  196. echo -e "${YELLOW}Latest CLI release: ${BOLD}$latest_cli_tag${NC}"
  197. echo ""
  198. echo -e "${CYAN}Available platforms:${NC}"
  199. echo "$response" | jq -r '.[] | select(.tag_name | endswith("-cli")) | .assets[].name' 2>/dev/null | grep "\.tar\.gz$" | head -5 | sed 's/^/ /'
  200. echo ""
  201. echo -e "${DIM}Visit: https://github.com/$GITHUB_REPO/releases/tag/$latest_cli_tag${NC}"
  202. echo ""
  203. exit 1
  204. fi
  205. cli_tag=$(echo "$release_info" | cut -d'|' -f1)
  206. download_url=$(echo "$release_info" | cut -d'|' -f2)
  207. else
  208. # Fallback: fetch specific release by tag (similar error handling)
  209. local releases_data=$(eval curl -fsSL $auth_header "https://api.github.com/repos/$GITHUB_REPO/releases" 2>&1)
  210. local curl_exit=$?
  211. if [ $curl_exit -ne 0 ]; then
  212. if check_rate_limit; then
  213. show_rate_limit_error
  214. exit 1
  215. fi
  216. print_error "Failed to fetch releases from GitHub"
  217. exit 1
  218. fi
  219. # Extract the first tag ending in -cli
  220. cli_tag=$(echo "$releases_data" | grep -o '"tag_name": "[^"]*-cli"' | head -1 | cut -d'"' -f4)
  221. if [ -z "$cli_tag" ]; then
  222. print_error "No CLI releases found"
  223. exit 1
  224. fi
  225. # Fetch the specific release to get assets
  226. local release_data=$(eval curl -fsSL $auth_header "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$cli_tag")
  227. # Extract download URL from the specific release
  228. download_url=$(echo "$release_data" | grep -o "\"browser_download_url\": \"[^\"]*${platform}[^\"]*\.tar\.gz\"" | head -1 | cut -d'"' -f4)
  229. fi
  230. print_ok "Found version ${MAGENTA}${BOLD}$cli_tag${NC}"
  231. else
  232. # Similar logic for specific version...
  233. print_step "Fetching version ${MAGENTA}$requested_version${NC}"
  234. cli_tag="$requested_version"
  235. local auth_header=""
  236. if [ -n "${GITHUB_TOKEN:-}" ]; then
  237. auth_header="-H \"Authorization: Bearer $GITHUB_TOKEN\""
  238. fi
  239. if command -v jq >/dev/null 2>&1; then
  240. download_url=$(eval curl -fsSL $auth_header "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$requested_version" | \
  241. jq -r '.assets[] | select(.name | contains("'"$platform"'") and endswith(".tar.gz")) | .browser_download_url' | head -1)
  242. else
  243. local release_data=$(eval curl -fsSL $auth_header "https://api.github.com/repos/$GITHUB_REPO/releases/tags/$requested_version")
  244. download_url=$(echo "$release_data" | grep -o "\"browser_download_url\": \"[^\"]*${platform}[^\"]*\.tar\.gz\"" | head -1 | cut -d'"' -f4)
  245. fi
  246. fi
  247. if [ -z "$download_url" ]; then
  248. print_error "Could not find $platform package in release $cli_tag"
  249. echo -e "${DIM}Visit: https://github.com/$GITHUB_REPO/releases/tag/$cli_tag${NC}"
  250. exit 1
  251. fi
  252. }
  253. # Check if already installed with same version
  254. check_existing_installation() {
  255. # Skip check if force install
  256. if [ "$FORCE_INSTALL" = "true" ]; then
  257. print_message "$YELLOW" "Force reinstalling..."
  258. echo ""
  259. return
  260. fi
  261. if [ -d "$INSTALL_DIR/bin" ] && [ -f "$INSTALL_DIR/bin/cline" ]; then
  262. # Extract version from cline binary
  263. local installed_version=$("$INSTALL_DIR/bin/cline" version 2>/dev/null | head -1 | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "")
  264. # Compare versions (remove -cli suffix for comparison)
  265. local cli_tag_version=$(echo "$cli_tag" | sed 's/-cli$//')
  266. if [ -n "$installed_version" ] && [ "$installed_version" = "$cli_tag_version" ]; then
  267. echo ""
  268. print_ok "Cline ${MAGENTA}${BOLD}$installed_version${NC} already installed"
  269. echo ""
  270. print_message "$DIM" "Installation directory: $INSTALL_DIR"
  271. print_message "$DIM" "To reinstall, run: ${MAGENTA}rm -rf $INSTALL_DIR && <install command>${NC}"
  272. print_message "$DIM" "Or use: ${MAGENTA}FORCE_INSTALL=true${NC} to force reinstall"
  273. echo ""
  274. exit 0
  275. elif [ -n "$installed_version" ]; then
  276. print_message "$YELLOW" "Upgrading from ${MAGENTA}$installed_version${YELLOW} to ${MAGENTA}${BOLD}$cli_tag_version${NC}"
  277. echo ""
  278. fi
  279. fi
  280. }
  281. # Download and install
  282. install_cline() {
  283. print_step "Installing Cline"
  284. # Create temporary directory
  285. local tmp_dir=$(mktemp -d)
  286. trap "rm -rf $tmp_dir" EXIT
  287. # Download with progress bar
  288. echo -e "${MAGENTA}${BOLD}"
  289. local package_file="$tmp_dir/cline.tar.gz"
  290. if ! curl -#fSL -o "$package_file" "$download_url"; then
  291. echo -e "${NC}"
  292. print_error "Failed to download package"
  293. echo -e "${DIM}URL: $download_url${NC}"
  294. exit 1
  295. fi
  296. echo -e "${NC}"
  297. # Verify download
  298. if [ ! -f "$package_file" ]; then
  299. print_error "Download failed: file not found"
  300. exit 1
  301. fi
  302. local file_size=$(stat -f%z "$package_file" 2>/dev/null || stat -c%s "$package_file" 2>/dev/null)
  303. print_ok "Downloaded $(numfmt --to=iec $file_size 2>/dev/null || echo "$file_size bytes")"
  304. # Remove existing installation
  305. if [ -d "$INSTALL_DIR" ]; then
  306. rm -rf "$INSTALL_DIR"
  307. fi
  308. # Create installation directory
  309. mkdir -p "$INSTALL_DIR"
  310. # Extract package
  311. print_step "Extracting package"
  312. if ! tar -xzf "$package_file" -C "$INSTALL_DIR" --strip-components=0; then
  313. print_error "Failed to extract package"
  314. exit 1
  315. fi
  316. # Make binaries executable
  317. if [ -d "$INSTALL_DIR/bin" ]; then
  318. chmod +x "$INSTALL_DIR/bin/"* 2>/dev/null || true
  319. else
  320. print_error "No bin directory found"
  321. echo -e "${DIM}Contents of $INSTALL_DIR:${NC}"
  322. ls -la "$INSTALL_DIR"
  323. exit 1
  324. fi
  325. # Copy platform-specific native modules
  326. if [ -d "$INSTALL_DIR/binaries/$platform/node_modules" ]; then
  327. if cp -r "$INSTALL_DIR/binaries/$platform/node_modules/"* "$INSTALL_DIR/node_modules/" 2>/dev/null; then
  328. print_ok "Native modules installed"
  329. fi
  330. fi
  331. print_ok "Cline installed to ${MAGENTA}${BOLD}$INSTALL_DIR${NC}"
  332. }
  333. # Configure PATH
  334. configure_path() {
  335. local bin_dir="$INSTALL_DIR/bin"
  336. local XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
  337. print_step "Configuring PATH"
  338. # Detect shell and config files
  339. local current_shell=$(basename "${SHELL:-bash}")
  340. local config_files=""
  341. case $current_shell in
  342. fish)
  343. config_files="$HOME/.config/fish/config.fish"
  344. ;;
  345. zsh)
  346. config_files="$HOME/.zshrc $HOME/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc"
  347. ;;
  348. bash)
  349. config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc"
  350. ;;
  351. ash|sh)
  352. config_files="$HOME/.profile /etc/profile"
  353. ;;
  354. *)
  355. config_files="$HOME/.profile"
  356. ;;
  357. esac
  358. # Find first existing config file
  359. local config_file=""
  360. for file in $config_files; do
  361. if [ -f "$file" ]; then
  362. config_file="$file"
  363. break
  364. fi
  365. done
  366. # Create default if none exists
  367. if [ -z "$config_file" ]; then
  368. case $current_shell in
  369. fish)
  370. config_file="$HOME/.config/fish/config.fish"
  371. mkdir -p "$(dirname "$config_file")"
  372. ;;
  373. zsh)
  374. config_file="$HOME/.zshrc"
  375. ;;
  376. *)
  377. config_file="$HOME/.bashrc"
  378. ;;
  379. esac
  380. touch "$config_file"
  381. fi
  382. # Add to config if not already present
  383. if ! grep -q "$bin_dir" "$config_file" 2>/dev/null; then
  384. case $current_shell in
  385. fish)
  386. echo -e "\n# Cline CLI\nfish_add_path $bin_dir" >> "$config_file"
  387. ;;
  388. *)
  389. echo -e "\n# Cline CLI\nexport PATH=\"$bin_dir:\$PATH\"" >> "$config_file"
  390. ;;
  391. esac
  392. print_ok "Added to PATH in ${CYAN}$(basename $config_file)${NC}"
  393. else
  394. print_ok "Already in PATH"
  395. fi
  396. # Add to GitHub Actions PATH if applicable
  397. if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
  398. echo "$bin_dir" >> "$GITHUB_PATH"
  399. fi
  400. }
  401. # Verify installation
  402. verify_installation() {
  403. print_step "Verifying installation"
  404. local cline_bin="$INSTALL_DIR/bin/cline"
  405. if [ ! -f "$cline_bin" ]; then
  406. print_error "Binary not found at $cline_bin"
  407. exit 1
  408. fi
  409. if [ ! -x "$cline_bin" ]; then
  410. chmod +x "$cline_bin"
  411. fi
  412. print_ok "Installation verified"
  413. }
  414. # Smart centered box printer
  415. # Smart centered box printer
  416. print_box() {
  417. local text=$1
  418. local color=$2
  419. local preferred_width=${3:-48} # Default preferred box width of 48
  420. # Try multiple methods to get terminal width
  421. local term_width_tput=$(tput cols 2>/dev/null || echo 0)
  422. local term_width_stty=$(stty size 2>/dev/null | cut -d' ' -f2 || echo 0)
  423. local term_width_env=${COLUMNS:-0}
  424. # Build array of valid widths
  425. local widths=()
  426. [ "$term_width_tput" -gt 0 ] 2>/dev/null && widths+=($term_width_tput)
  427. [ "$term_width_stty" -gt 0 ] 2>/dev/null && widths+=($term_width_stty)
  428. [ "$term_width_env" -gt 0 ] 2>/dev/null && widths+=($term_width_env)
  429. # Smart selection logic
  430. local term_width=80 # fallback
  431. if [ ${#widths[@]} -gt 0 ]; then
  432. # Find min and max
  433. local min_width=${widths[0]}
  434. local max_width=${widths[0]}
  435. for width in "${widths[@]}"; do
  436. if [ "$width" -lt "$min_width" ]; then
  437. min_width=$width
  438. fi
  439. if [ "$width" -gt "$max_width" ]; then
  440. max_width=$width
  441. fi
  442. done
  443. # If any width is less than preferred (48), use the smallest (most conservative)
  444. # Otherwise, use the largest (give more space)
  445. if [ "$min_width" -lt "$preferred_width" ]; then
  446. term_width=$min_width
  447. else
  448. term_width=$max_width
  449. fi
  450. fi
  451. # Ensure we have a valid number and reasonable minimum
  452. if ! [[ "$term_width" =~ ^[0-9]+$ ]] || [ "$term_width" -lt 20 ]; then
  453. term_width=80
  454. fi
  455. # Calculate content width (length of longest line in text)
  456. local content_width=0
  457. while IFS= read -r line; do
  458. # Strip ANSI color codes for accurate length measurement
  459. local clean_line=$(echo "$line" | sed 's/\x1b\[[0-9;]*m//g')
  460. local line_length=${#clean_line}
  461. if [ $line_length -gt $content_width ]; then
  462. content_width=$line_length
  463. fi
  464. done <<< "$text"
  465. # Calculate max possible box width (terminal width - 4 chars total padding minimum)
  466. local max_box_width=$((term_width - 4))
  467. # Determine internal text padding based on box width
  468. # For narrow boxes (< 30), use 1 space padding; otherwise 2 spaces
  469. local text_padding_size=2
  470. if [ $max_box_width -lt 30 ]; then
  471. text_padding_size=1
  472. fi
  473. # Start with preferred width, but respect terminal constraints
  474. local box_width=$preferred_width
  475. if [ $box_width -gt $max_box_width ]; then
  476. box_width=$max_box_width
  477. fi
  478. # Ensure box is at least wide enough for content (with adaptive padding)
  479. local min_width=$((content_width + (text_padding_size * 2) + 2)) # content + padding + borders
  480. if [ $box_width -lt $min_width ]; then
  481. box_width=$min_width
  482. # If even min_width exceeds terminal, shrink to fit
  483. if [ $box_width -gt $max_box_width ]; then
  484. box_width=$max_box_width
  485. fi
  486. fi
  487. # Absolute minimum box width
  488. if [ $box_width -lt 10 ]; then
  489. box_width=10
  490. fi
  491. # Calculate horizontal padding to center the box in terminal
  492. local box_padding=$(( (term_width - box_width) / 2 ))
  493. if [ $box_padding -lt 1 ]; then
  494. box_padding=1 # Ensure at least 1 character padding
  495. fi
  496. # Build horizontal line
  497. local horizontal_line="═"
  498. for ((i=1; i<box_width-2; i++)); do
  499. horizontal_line+="═"
  500. done
  501. # Decide whether to include empty lines based on box height constraints
  502. local include_empty_lines=true
  503. if [ $box_width -lt 25 ]; then
  504. include_empty_lines=false # Skip empty lines for very narrow boxes
  505. fi
  506. # Print top border
  507. printf "%${box_padding}s" ""
  508. echo -e "${color}╔${horizontal_line}╗${NC}"
  509. # Print empty line (optional)
  510. if [ "$include_empty_lines" = true ]; then
  511. printf "%${box_padding}s" ""
  512. printf "${color}║"
  513. printf "%$((box_width-2))s" ""
  514. printf "║${NC}\n"
  515. fi
  516. # Print content lines (centered)
  517. while IFS= read -r line; do
  518. # Strip ANSI codes for length calculation
  519. local clean_line=$(echo "$line" | sed 's/\x1b\[[0-9;]*m//g')
  520. local line_length=${#clean_line}
  521. # Calculate padding with adaptive spacing
  522. local left_text_padding=$(( (box_width - 2 - line_length) / 2 ))
  523. local right_text_padding=$((box_width - 2 - line_length - left_text_padding))
  524. printf "%${box_padding}s" ""
  525. printf "${color}║"
  526. printf "%${left_text_padding}s" ""
  527. printf "%s" "$line"
  528. printf "%${right_text_padding}s" ""
  529. printf "║${NC}\n"
  530. done <<< "$text"
  531. # Print empty line (optional)
  532. if [ "$include_empty_lines" = true ]; then
  533. printf "%${box_padding}s" ""
  534. printf "${color}║"
  535. printf "%$((box_width-2))s" ""
  536. printf "║${NC}\n"
  537. fi
  538. # Print bottom border
  539. printf "%${box_padding}s" ""
  540. echo -e "${color}╚${horizontal_line}╝${NC}"
  541. }
  542. # Print success message
  543. print_success() {
  544. echo ""
  545. print_box "Installation complete" "$GREEN$BOLD" 48
  546. echo ""
  547. print_message "$NC" "Run this to start using ${MAGENTA}${BOLD}cline${NC} immediately:"
  548. echo ""
  549. print_message "$YELLOW" "${BOLD} exec \$SHELL"
  550. echo ""
  551. print_message "$DIM" "(or just open a new terminal window)"
  552. echo ""
  553. }
  554. # Main installation flow
  555. main() {
  556. echo ""
  557. print_box "CLINE IS COOKING" "$MAGENTA$BOLD" 48
  558. echo ""
  559. print_ok "Platform: ${MAGENTA}${BOLD}$platform${NC}"
  560. check_prerequisites
  561. get_release_info
  562. check_existing_installation
  563. install_cline
  564. configure_path
  565. verify_installation
  566. print_success
  567. }
  568. # Run main function
  569. main "$@"