install.sh 11 KB

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