install.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. REPO_OWNER="christianlempa"
  4. REPO_NAME="boilerplates"
  5. INSTALL_VERSION="${VERSION:-${INSTALL_VERSION:-latest}}"
  6. AUTO_INSTALL="${AUTO_INSTALL:-true}"
  7. OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}"
  8. usage() {
  9. cat <<USAGE
  10. Usage: install.sh [OPTIONS]
  11. Install the boilerplates CLI from GitHub releases via pipx.
  12. Options:
  13. --version VER Version to install (default: "latest")
  14. --no-auto-install Skip automatic dependency installation
  15. -h, --help Show this message
  16. Examples:
  17. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  18. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash -s -- --version v1.0.0
  19. Uninstall:
  20. pipx uninstall boilerplates
  21. USAGE
  22. }
  23. log() { printf '[boilerplates] %s\n' "$*" >&2; }
  24. error() { printf '[boilerplates][error] %s\n' "$*" >&2; exit 1; }
  25. detect_os() {
  26. if [[ "$OSTYPE" == "darwin"* ]]; then
  27. OS_TYPE="macos"
  28. elif [[ -f "$OS_RELEASE_FILE" ]]; then
  29. OS_TYPE="linux"
  30. . "$OS_RELEASE_FILE"
  31. DISTRO_ID="$ID"
  32. DISTRO_VERSION="${VERSION_ID:-}"
  33. else
  34. OS_TYPE="unknown"
  35. fi
  36. }
  37. install_dependencies_macos() {
  38. log "Detected macOS"
  39. if ! command -v brew >/dev/null 2>&1; then
  40. log "Homebrew not found. Installing Homebrew..."
  41. /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || error "Failed to install Homebrew"
  42. fi
  43. if ! command -v python3 >/dev/null 2>&1; then
  44. log "Installing Python3..."
  45. brew install python3 || error "Failed to install Python3"
  46. fi
  47. if ! command -v git >/dev/null 2>&1; then
  48. log "Installing git..."
  49. brew install git || error "Failed to install git"
  50. fi
  51. if ! command -v pipx >/dev/null 2>&1; then
  52. log "Installing pipx..."
  53. brew install pipx || error "Failed to install pipx"
  54. pipx ensurepath
  55. fi
  56. }
  57. install_dependencies_linux() {
  58. log "Detected Linux ($DISTRO_ID)"
  59. case "$DISTRO_ID" in
  60. ubuntu|debian|pop|linuxmint|elementary)
  61. PKG_MANAGER="apt"
  62. PYTHON_PKG="python3 python3-pip python3-venv"
  63. PIPX_PKG="pipx"
  64. GIT_PKG="git"
  65. UPDATE_CMD="sudo apt update"
  66. INSTALL_CMD="sudo apt install -y"
  67. ;;
  68. fedora|rhel|centos|rocky|almalinux)
  69. PKG_MANAGER="dnf"
  70. PYTHON_PKG="python3 python3-pip"
  71. PIPX_PKG="pipx"
  72. GIT_PKG="git"
  73. UPDATE_CMD="sudo dnf check-update || true"
  74. INSTALL_CMD="sudo dnf install -y"
  75. ;;
  76. opensuse*|sles)
  77. PKG_MANAGER="zypper"
  78. PYTHON_PKG="python3 python3-pip"
  79. PIPX_PKG="python3-pipx"
  80. GIT_PKG="git"
  81. UPDATE_CMD="sudo zypper refresh"
  82. INSTALL_CMD="sudo zypper install -y"
  83. ;;
  84. arch|archarm|manjaro|endeavouros)
  85. PKG_MANAGER="pacman"
  86. PYTHON_PKG="python python-pip"
  87. PIPX_PKG="python-pipx"
  88. GIT_PKG="git"
  89. UPDATE_CMD="sudo pacman -Sy"
  90. INSTALL_CMD="sudo pacman -S --noconfirm"
  91. ;;
  92. alpine)
  93. PKG_MANAGER="apk"
  94. PYTHON_PKG="python3 py3-pip"
  95. PIPX_PKG="pipx"
  96. GIT_PKG="git"
  97. UPDATE_CMD="sudo apk update"
  98. INSTALL_CMD="sudo apk add"
  99. ;;
  100. *)
  101. log "Unsupported Linux distribution: $DISTRO_ID"
  102. log "Please install manually: python3, pip, git, and pipx"
  103. return 1
  104. ;;
  105. esac
  106. if ! command -v python3 >/dev/null 2>&1; then
  107. log "Installing Python3..."
  108. $UPDATE_CMD
  109. $INSTALL_CMD $PYTHON_PKG || error "Failed to install Python3"
  110. fi
  111. if ! command -v git >/dev/null 2>&1; then
  112. log "Installing git..."
  113. $INSTALL_CMD $GIT_PKG || error "Failed to install git"
  114. fi
  115. if ! python3 -m pip --version >/dev/null 2>&1; then
  116. log "pip not available, installing..."
  117. $INSTALL_CMD $PYTHON_PKG || error "Failed to install pip"
  118. fi
  119. if ! command -v pipx >/dev/null 2>&1 && [[ ! -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  120. log "Installing pipx..."
  121. # Try system package first if available
  122. if [[ -n "${PIPX_PKG:-}" ]]; then
  123. if $INSTALL_CMD $PIPX_PKG >/dev/null 2>&1; then
  124. log "pipx installed from system package"
  125. else
  126. # System package failed, try pip with --break-system-packages
  127. if python3 -m pip install --user --break-system-packages pipx 2>&1 | grep -q "Successfully installed"; then
  128. log "pipx installed via pip"
  129. elif python3 -m pip install --user pipx 2>&1 | grep -q "Successfully installed"; then
  130. log "pipx installed via pip"
  131. else
  132. error "Failed to install pipx. Try installing manually: sudo apt install pipx"
  133. fi
  134. fi
  135. else
  136. # No system package, use pip
  137. if python3 -m pip install --user --break-system-packages pipx 2>&1 | grep -q "Successfully installed"; then
  138. log "pipx installed via pip"
  139. elif python3 -m pip install --user pipx 2>&1 | grep -q "Successfully installed"; then
  140. log "pipx installed via pip"
  141. else
  142. error "Failed to install pipx"
  143. fi
  144. fi
  145. # Ensure pipx is in PATH
  146. if command -v pipx >/dev/null 2>&1; then
  147. pipx ensurepath >/dev/null 2>&1
  148. elif [[ -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  149. "$(python3 -m site --user-base)/bin/pipx" ensurepath >/dev/null 2>&1
  150. fi
  151. fi
  152. }
  153. check_dependencies() {
  154. local missing_deps=()
  155. command -v tar >/dev/null 2>&1 || missing_deps+=("tar")
  156. command -v mktemp >/dev/null 2>&1 || missing_deps+=("mktemp")
  157. if [[ ${#missing_deps[@]} -gt 0 ]]; then
  158. error "Required system tools missing: ${missing_deps[*]}"
  159. fi
  160. local needs_install=false
  161. if ! command -v python3 >/dev/null 2>&1; then
  162. log "Python3 not found"
  163. needs_install=true
  164. fi
  165. if ! command -v git >/dev/null 2>&1; then
  166. log "git not found"
  167. needs_install=true
  168. fi
  169. if ! python3 -m pip --version >/dev/null 2>&1; then
  170. log "pip not found"
  171. needs_install=true
  172. fi
  173. if ! command -v pipx >/dev/null 2>&1 && [[ ! -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  174. log "pipx not found"
  175. needs_install=true
  176. fi
  177. if [[ "$needs_install" == "true" ]]; then
  178. if [[ "$AUTO_INSTALL" == "true" ]]; then
  179. log "Installing missing dependencies..."
  180. detect_os
  181. if [[ "$OS_TYPE" == "macos" ]]; then
  182. install_dependencies_macos
  183. elif [[ "$OS_TYPE" == "linux" ]]; then
  184. install_dependencies_linux
  185. else
  186. error "Unsupported OS. Please install manually: python3, pip, git, and pipx"
  187. fi
  188. else
  189. error "Missing dependencies. Install: python3, pip, git, and pipx (or run without --no-auto-install)"
  190. fi
  191. fi
  192. if command -v pipx >/dev/null 2>&1; then
  193. PIPX_CMD="pipx"
  194. elif [[ -x "$(python3 -m site --user-base 2>/dev/null)/bin/pipx" ]]; then
  195. PIPX_CMD="$(python3 -m site --user-base)/bin/pipx"
  196. else
  197. error "pipx installation failed or not found in PATH. Try: python3 -m pip install --user pipx && python3 -m pipx ensurepath"
  198. fi
  199. log "All dependencies available"
  200. }
  201. parse_args() {
  202. while [[ $# -gt 0 ]]; do
  203. case "$1" in
  204. --version)
  205. [[ $# -lt 2 ]] && error "--version requires an argument"
  206. [[ "$2" =~ ^- ]] && error "--version requires a version string, not an option"
  207. INSTALL_VERSION="$2"
  208. shift 2
  209. ;;
  210. --no-auto-install)
  211. AUTO_INSTALL="false"
  212. shift
  213. ;;
  214. -h|--help) usage; exit 0 ;;
  215. *) error "Unknown option: $1" ;;
  216. esac
  217. done
  218. }
  219. get_latest_release() {
  220. local api_url="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
  221. local result
  222. if command -v curl >/dev/null 2>&1; then
  223. result=$(curl -qfsSL --max-time 10 "$api_url" 2>/dev/null | sed -En 's/.*"tag_name": "([^"]+)".*/\1/p')
  224. elif command -v wget >/dev/null 2>&1; then
  225. result=$(wget --timeout=10 -qO- "$api_url" 2>/dev/null | sed -En 's/.*"tag_name": "([^"]+)".*/\1/p')
  226. else
  227. error "Neither curl nor wget found"
  228. fi
  229. [[ -z "$result" ]] && error "Failed to fetch release information from GitHub"
  230. echo "$result"
  231. }
  232. download_and_extract() {
  233. local version="$1"
  234. # Resolve "latest" to actual version
  235. if [[ "$version" == "latest" ]]; then
  236. log "Fetching latest release..."
  237. version=$(get_latest_release)
  238. log "Latest version: $version"
  239. fi
  240. # Ensure 'v' prefix for URL
  241. local version_tag="$version"
  242. [[ "$version_tag" =~ ^v ]] || version_tag="v$version_tag"
  243. # Strip 'v' prefix for package name
  244. local version_number="${version_tag#v}"
  245. # Download from release assets (sdist)
  246. local url="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/$version_tag/$REPO_NAME-$version_number.tar.gz"
  247. TEMP_DIR=$(mktemp -d)
  248. local archive="$TEMP_DIR/boilerplates.tar.gz"
  249. log "Downloading $version_tag from release assets..."
  250. if command -v curl >/dev/null 2>&1; then
  251. curl -qfsSL --max-time 30 -o "$archive" "$url" || error "Download failed. URL: $url"
  252. elif command -v wget >/dev/null 2>&1; then
  253. wget --timeout=30 -qO "$archive" "$url" || error "Download failed. URL: $url"
  254. fi
  255. log "Extracting package..."
  256. # Extract the tarball
  257. tar -xzf "$archive" -C "$TEMP_DIR" || error "Extraction failed"
  258. # Find the extracted directory (should be boilerplates-X.Y.Z)
  259. local source_dir=$(find "$TEMP_DIR" -maxdepth 1 -type d -name "$REPO_NAME-*" | head -n1)
  260. [[ -z "$source_dir" ]] && error "Failed to locate extracted files"
  261. # Verify essential files exist
  262. [[ ! -f "$source_dir/setup.py" ]] && [[ ! -f "$source_dir/pyproject.toml" ]] && \
  263. error "Invalid package: missing setup.py or pyproject.toml"
  264. # Return the path to the extracted directory
  265. echo "$source_dir"
  266. }
  267. install_cli() {
  268. local package_path="$1"
  269. local version="$2"
  270. log "Installing CLI via pipx..."
  271. "$PIPX_CMD" ensurepath 2>&1 | grep -v "^$" || true
  272. # Install from tarball
  273. if ! "$PIPX_CMD" install --force "$package_path" >/dev/null 2>&1; then
  274. error "pipx installation failed. Try: pipx uninstall boilerplates && pipx install boilerplates"
  275. fi
  276. log "CLI installed successfully"
  277. # Verify installation
  278. if command -v boilerplates >/dev/null 2>&1; then
  279. log "Command 'boilerplates' is now available"
  280. else
  281. log "Warning: 'boilerplates' command not found in PATH. You may need to restart your shell or run: pipx ensurepath"
  282. fi
  283. }
  284. main() {
  285. parse_args "$@"
  286. # Ensure cleanup on exit
  287. trap '[[ -d "${TEMP_DIR:-}" ]] && rm -rf "$TEMP_DIR"' EXIT
  288. log "Checking dependencies..."
  289. check_dependencies
  290. local package_path
  291. package_path=$(download_and_extract "$INSTALL_VERSION")
  292. install_cli "$package_path" "$INSTALL_VERSION"
  293. # Get installed version
  294. local installed_version=$(boilerplates --version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown")
  295. cat <<EOF
  296. \uf05d Installation complete!
  297. Version: $installed_version
  298. Installed via: pipx
  299. Usage:
  300. boilerplates --help
  301. Update:
  302. curl -qfsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  303. Uninstall:
  304. pipx uninstall boilerplates
  305. EOF
  306. }
  307. if [[ "${BASH_SOURCE[0]-$0}" == "$0" ]]; then
  308. main "$@"
  309. fi