install.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. REPO_OWNER="christianlempa"
  4. REPO_NAME="boilerplates"
  5. VERSION="${VERSION:-latest}"
  6. TARGET_DIR="${TARGET_DIR:-$HOME/boilerplates}"
  7. usage() {
  8. cat <<USAGE
  9. Usage: install.sh [OPTIONS]
  10. Install the boilerplates CLI from GitHub releases.
  11. Options:
  12. --path DIR Installation directory (default: "$HOME/boilerplates")
  13. --version VER Version to install (default: "latest")
  14. Examples: latest, v1.0.0, v0.0.1
  15. -h, --help Show this message
  16. Examples:
  17. # Install latest version
  18. curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash
  19. # Install specific version
  20. curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --version v1.0.0
  21. # Install to custom directory
  22. curl -fsSL https://raw.githubusercontent.com/christianlempa/boilerplates/main/scripts/install.sh | bash -s -- --path ~/my-boilerplates
  23. USAGE
  24. }
  25. log() {
  26. printf '[boilerplates] %s\n' "$*" >&2
  27. }
  28. error() {
  29. printf '[boilerplates][error] %s\n' "$*" >&2
  30. exit 1
  31. }
  32. warn() {
  33. printf '[boilerplates][warn] %s\n' "$*" >&2
  34. }
  35. detect_os() {
  36. if [[ "$OSTYPE" == "linux-gnu"* ]]; then
  37. if command -v apt-get >/dev/null 2>&1; then
  38. echo "debian"
  39. elif command -v dnf >/dev/null 2>&1; then
  40. echo "fedora"
  41. elif command -v yum >/dev/null 2>&1; then
  42. echo "rhel"
  43. elif command -v pacman >/dev/null 2>&1; then
  44. echo "arch"
  45. else
  46. echo "linux"
  47. fi
  48. elif [[ "$OSTYPE" == "darwin"* ]]; then
  49. echo "macos"
  50. else
  51. echo "unknown"
  52. fi
  53. }
  54. ensure_python3() {
  55. if command -v python3 >/dev/null 2>&1; then
  56. log "✓ Python3 is already installed"
  57. return 0
  58. fi
  59. log "Python3 not found. Attempting to install..."
  60. local os_type
  61. os_type=$(detect_os)
  62. case "$os_type" in
  63. debian)
  64. log "Detected Debian/Ubuntu. Installing python3..."
  65. sudo apt-get update && sudo apt-get install -y python3 python3-pip python3-venv
  66. ;;
  67. fedora)
  68. log "Detected Fedora. Installing python3..."
  69. sudo dnf install -y python3 python3-pip
  70. ;;
  71. rhel)
  72. log "Detected RHEL/CentOS. Installing python3..."
  73. sudo yum install -y python3 python3-pip
  74. ;;
  75. arch)
  76. log "Detected Arch Linux. Installing python3..."
  77. sudo pacman -S --noconfirm python python-pip
  78. ;;
  79. macos)
  80. if command -v brew >/dev/null 2>&1; then
  81. log "Detected macOS with Homebrew. Installing python3..."
  82. brew install python3
  83. else
  84. error "Python3 not found and Homebrew is not installed. Please install Python3 manually from https://www.python.org/downloads/"
  85. fi
  86. ;;
  87. *)
  88. error "Could not automatically install Python3 on this system. Please install Python3 manually."
  89. ;;
  90. esac
  91. # Verify installation
  92. if ! command -v python3 >/dev/null 2>&1; then
  93. error "Failed to install Python3. Please install it manually."
  94. fi
  95. log "✓ Python3 installed successfully"
  96. }
  97. ensure_pip() {
  98. if python3 -m pip --version >/dev/null 2>&1; then
  99. log "✓ pip is already installed"
  100. return 0
  101. fi
  102. log "pip not found. Attempting to install..."
  103. # Detect OS and install pip using system package manager (preferred for modern Python)
  104. local os_type
  105. os_type=$(detect_os)
  106. case "$os_type" in
  107. debian)
  108. log "Installing pip via apt..."
  109. if sudo apt-get update && sudo apt-get install -y python3-pip python3-venv; then
  110. log "✓ pip installed via apt"
  111. else
  112. error "Failed to install pip via apt. Please run: sudo apt-get install python3-pip python3-venv"
  113. fi
  114. ;;
  115. fedora)
  116. log "Installing pip via dnf..."
  117. if sudo dnf install -y python3-pip; then
  118. log "✓ pip installed via dnf"
  119. else
  120. error "Failed to install pip via dnf. Please run: sudo dnf install python3-pip"
  121. fi
  122. ;;
  123. rhel)
  124. log "Installing pip via yum..."
  125. if sudo yum install -y python3-pip; then
  126. log "✓ pip installed via yum"
  127. else
  128. error "Failed to install pip via yum. Please run: sudo yum install python3-pip"
  129. fi
  130. ;;
  131. arch)
  132. log "Installing pip via pacman..."
  133. if sudo pacman -S --noconfirm python-pip; then
  134. log "✓ pip installed via pacman"
  135. else
  136. error "Failed to install pip via pacman. Please run: sudo pacman -S python-pip"
  137. fi
  138. ;;
  139. macos)
  140. # On macOS, pip usually comes with Python from Homebrew
  141. log "pip should be included with Python. Verifying..."
  142. python3 -m ensurepip --default-pip 2>/dev/null || true
  143. ;;
  144. *)
  145. error "Could not detect your OS or package manager. Please install pip manually: sudo apt install python3-pip (Debian/Ubuntu) or equivalent for your system."
  146. ;;
  147. esac
  148. # Verify installation
  149. if ! python3 -m pip --version >/dev/null 2>&1; then
  150. error "Failed to install pip. Please install it manually using your system package manager."
  151. fi
  152. log "✓ pip installed successfully"
  153. }
  154. require_command() {
  155. command -v "$1" >/dev/null 2>&1 || error "Required command '$1' not found in PATH"
  156. }
  157. parse_args() {
  158. while [[ $# -gt 0 ]]; do
  159. case "$1" in
  160. --path)
  161. [[ $# -lt 2 ]] && error "--path requires a value"
  162. TARGET_DIR="$2"
  163. shift 2
  164. ;;
  165. --version)
  166. [[ $# -lt 2 ]] && error "--version requires a value"
  167. VERSION="$2"
  168. shift 2
  169. ;;
  170. -h|--help)
  171. usage
  172. exit 0
  173. ;;
  174. *)
  175. error "Unknown option: $1"
  176. ;;
  177. esac
  178. done
  179. }
  180. make_absolute_path() {
  181. python3 - <<'PY' "$TARGET_DIR"
  182. import os, sys
  183. print(os.path.abspath(os.path.expanduser(sys.argv[1])))
  184. PY
  185. }
  186. get_latest_release() {
  187. local api_url="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/latest"
  188. local release_tag
  189. # Note: Don't use log() here as this function's output is captured
  190. # The calling function will log the result
  191. if command -v curl >/dev/null 2>&1; then
  192. release_tag=$(curl -fsSL "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
  193. elif command -v wget >/dev/null 2>&1; then
  194. release_tag=$(wget -qO- "$api_url" | grep '"tag_name":' | sed -E 's/.*"tag_name": "([^"]+)".*/\1/')
  195. else
  196. echo "error: Neither curl nor wget found" >&2
  197. return 1
  198. fi
  199. if [[ -z "$release_tag" ]]; then
  200. echo "error: Failed to fetch latest release tag" >&2
  201. return 1
  202. fi
  203. echo "$release_tag"
  204. }
  205. download_release() {
  206. local version="$1"
  207. local download_url
  208. # If version is "latest", resolve it to the actual version tag
  209. if [[ "$version" == "latest" ]]; then
  210. log "Fetching latest release information..."
  211. version=$(get_latest_release)
  212. local exit_code=$?
  213. if [[ $exit_code -ne 0 ]] || [[ -z "$version" ]] || [[ "$version" == error:* ]]; then
  214. error "Failed to fetch latest release information"
  215. fi
  216. log "Latest version is $version"
  217. fi
  218. # Ensure version has 'v' prefix for GitHub releases
  219. if [[ ! "$version" =~ ^v ]]; then
  220. version="v$version"
  221. fi
  222. download_url="https://github.com/$REPO_OWNER/$REPO_NAME/archive/refs/tags/$version.tar.gz"
  223. log "Downloading release $version..."
  224. log "URL: $download_url"
  225. local temp_dir
  226. temp_dir=$(mktemp -d)
  227. local archive_file="$temp_dir/boilerplates.tar.gz"
  228. if command -v curl >/dev/null 2>&1; then
  229. curl -fsSL -o "$archive_file" "$download_url" || {
  230. rm -rf "$temp_dir"
  231. error "Failed to download release"
  232. }
  233. elif command -v wget >/dev/null 2>&1; then
  234. wget -qO "$archive_file" "$download_url" || {
  235. rm -rf "$temp_dir"
  236. error "Failed to download release"
  237. }
  238. else
  239. rm -rf "$temp_dir"
  240. error "Neither curl nor wget found. Please install one of them."
  241. fi
  242. log "Extracting release..."
  243. # Remove existing installation if present
  244. if [[ -d "$TARGET_DIR" ]]; then
  245. log "Removing existing installation at $TARGET_DIR"
  246. rm -rf "$TARGET_DIR"
  247. fi
  248. # Create parent directory
  249. mkdir -p "$(dirname "$TARGET_DIR")"
  250. # Extract with strip-components to remove the top-level directory
  251. tar -xzf "$archive_file" -C "$(dirname "$TARGET_DIR")" || {
  252. rm -rf "$temp_dir"
  253. error "Failed to extract release"
  254. }
  255. # Rename extracted directory to target name
  256. local extracted_dir
  257. extracted_dir=$(dirname "$TARGET_DIR")/"$REPO_NAME-${version#v}"
  258. if [[ ! -d "$extracted_dir" ]]; then
  259. rm -rf "$temp_dir"
  260. error "Extraction failed: expected directory $extracted_dir not found"
  261. fi
  262. mv "$extracted_dir" "$TARGET_DIR" || {
  263. rm -rf "$temp_dir"
  264. error "Failed to move extracted files to $TARGET_DIR"
  265. }
  266. # Clean up temp directory
  267. rm -rf "$temp_dir"
  268. log "Release extracted to $TARGET_DIR"
  269. # Store version info
  270. echo "$version" > "$TARGET_DIR/.installed-version"
  271. }
  272. ensure_pipx() {
  273. if command -v pipx >/dev/null 2>&1; then
  274. log "✓ pipx is already installed"
  275. PIPX_CMD="pipx"
  276. return
  277. fi
  278. log "pipx not found. Installing pipx..."
  279. # Detect OS and try to install via package manager first (preferred for PEP 668 systems)
  280. local os_type
  281. os_type=$(detect_os)
  282. case "$os_type" in
  283. debian)
  284. log "Installing pipx via apt..."
  285. if sudo apt-get update && sudo apt-get install -y pipx; then
  286. log "✓ pipx installed via apt"
  287. # Ensure pipx path is set up
  288. pipx ensurepath >/dev/null 2>&1 || true
  289. else
  290. warn "Failed to install pipx via apt, trying pip..."
  291. install_pipx_with_pip
  292. fi
  293. ;;
  294. fedora)
  295. log "Installing pipx via dnf..."
  296. if sudo dnf install -y pipx; then
  297. log "✓ pipx installed via dnf"
  298. else
  299. warn "Failed to install pipx via dnf, trying pip..."
  300. install_pipx_with_pip
  301. fi
  302. ;;
  303. arch)
  304. log "Installing pipx via pacman..."
  305. if sudo pacman -S --noconfirm python-pipx; then
  306. log "✓ pipx installed via pacman"
  307. else
  308. warn "Failed to install pipx via pacman, trying pip..."
  309. install_pipx_with_pip
  310. fi
  311. ;;
  312. macos)
  313. if command -v brew >/dev/null 2>&1; then
  314. log "Installing pipx via Homebrew..."
  315. if brew install pipx; then
  316. log "✓ pipx installed via Homebrew"
  317. # Ensure pipx path is set up
  318. pipx ensurepath >/dev/null 2>&1 || true
  319. else
  320. warn "Failed to install pipx via Homebrew, trying pip..."
  321. install_pipx_with_pip
  322. fi
  323. else
  324. log "Homebrew not found, installing pipx via pip..."
  325. install_pipx_with_pip
  326. fi
  327. ;;
  328. *)
  329. # Fallback to pip installation
  330. install_pipx_with_pip
  331. ;;
  332. esac
  333. # Try to find pipx command
  334. if command -v pipx >/dev/null 2>&1; then
  335. PIPX_CMD="pipx"
  336. log "✓ pipx is ready to use"
  337. return
  338. fi
  339. # Check in user bin directory
  340. local user_bin
  341. user_bin="$(python3 -m site --user-base 2>/dev/null)/bin"
  342. if [[ -x "$user_bin/pipx" ]]; then
  343. PIPX_CMD="$user_bin/pipx"
  344. log "✓ Found pipx at $PIPX_CMD"
  345. return
  346. fi
  347. error "pipx installed but not found in PATH. Please add $(python3 -m site --user-base)/bin to your PATH or restart your shell."
  348. }
  349. install_pipx_with_pip() {
  350. log "Installing pipx using pip..."
  351. # Try with --user flag first (works on most systems)
  352. if python3 -m pip install --user pipx 2>/dev/null; then
  353. log "✓ pipx installed via pip --user"
  354. return
  355. fi
  356. # If that fails due to externally-managed-environment, try with --break-system-packages
  357. # (only as a last resort and with clear warning)
  358. warn "System has PEP 668 restrictions. Attempting installation with --break-system-packages..."
  359. if python3 -m pip install --user --break-system-packages pipx 2>/dev/null; then
  360. log "✓ pipx installed (with --break-system-packages)"
  361. return
  362. fi
  363. error "Failed to install pipx. Please install it manually: sudo apt install pipx (Debian/Ubuntu) or pip install --user pipx"
  364. }
  365. pipx_install() {
  366. log "Configuring pipx PATH..."
  367. "${PIPX_CMD}" ensurepath 2>&1 | grep -v "^$" || true
  368. log "Installing/updating boilerplates via pipx"
  369. "${PIPX_CMD}" install --editable --force "$TARGET_DIR"
  370. }
  371. check_current_version() {
  372. if [[ -f "$TARGET_DIR/.installed-version" ]]; then
  373. cat "$TARGET_DIR/.installed-version"
  374. else
  375. echo "unknown"
  376. fi
  377. }
  378. main() {
  379. parse_args "$@"
  380. log "Checking system dependencies..."
  381. # Ensure required tools are available
  382. require_command tar
  383. # Ensure Python3, pip, and pipx are installed
  384. ensure_python3
  385. ensure_pip
  386. TARGET_DIR="$(make_absolute_path)"
  387. # Check if already installed
  388. local current_version
  389. current_version=$(check_current_version)
  390. if [[ "$current_version" != "unknown" ]]; then
  391. log "Currently installed version: $current_version"
  392. fi
  393. download_release "$VERSION"
  394. ensure_pipx
  395. pipx_install
  396. local pipx_info
  397. pipx_info=$("${PIPX_CMD}" list --short 2>/dev/null | grep -E '^boilerplates' || echo "boilerplates (not detected)")
  398. local installed_version
  399. installed_version=$(check_current_version)
  400. # Check if boilerplates is in PATH
  401. local path_warning=""
  402. if ! command -v boilerplates >/dev/null 2>&1; then
  403. local user_bin
  404. user_bin="$(python3 -m site --user-base 2>/dev/null)/bin"
  405. # Detect shell and provide appropriate instructions
  406. local shell_config
  407. if [[ -n "${BASH_VERSION:-}" ]]; then
  408. shell_config="~/.bashrc"
  409. elif [[ -n "${ZSH_VERSION:-}" ]]; then
  410. shell_config="~/.zshrc"
  411. else
  412. shell_config="your shell's config file"
  413. fi
  414. path_warning=$(cat <<'PATHWARN'
  415. ⚠️ PATH Configuration Required
  416. The 'boilerplates' command is not in your PATH. To use it, add this to SHELL_CONFIG:
  417. export PATH="$PATH:USER_BIN"
  418. Then reload your shell:
  419. source SHELL_CONFIG
  420. Or use the full path for now:
  421. USER_BIN/boilerplates --help
  422. PATHWARN
  423. )
  424. path_warning="${path_warning//SHELL_CONFIG/$shell_config}"
  425. path_warning="${path_warning//USER_BIN/$user_bin}"
  426. fi
  427. cat <<EOF2
  428. ✓ Installation complete!
  429. Version: $installed_version
  430. Location: $TARGET_DIR
  431. pipx environment: $pipx_info$path_warning
  432. To use the CLI:
  433. boilerplates --help
  434. boilerplates compose list
  435. To update to the latest version:
  436. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash
  437. To install a specific version:
  438. curl -fsSL https://raw.githubusercontent.com/$REPO_OWNER/$REPO_NAME/main/scripts/install.sh | bash -s -- --version v1.0.0
  439. EOF2
  440. }
  441. main "$@"