diff --git a/b.sh b/b.sh index e979e77..a3e8668 100755 --- a/b.sh +++ b/b.sh @@ -1,35 +1,33 @@ # >>> bootstrap-cli b function >>> # Shortcut for downloading and running bootstrap/install scripts b() { - if [ -z "$1" ]; then + if [ -z "${1:-}" ]; then echo "Usage: b [args...]" >&2 return 1 fi local routes_dir="$HOME/.config/bootstrap" - local b_file="$routes_dir/b.sh" local routes_file="$routes_dir/routes.sh" local last_update_file="$routes_dir/.last_b_update" - # 1. Check for b.sh updates (once every 24 hours) local current_time current_time=$(date +%s 2>/dev/null || date +%s) local last_update=0 [ -f "$last_update_file" ] && last_update=$(cat "$last_update_file" 2>/dev/null || echo 0) - if [ $((current_time - last_update)) -gt 86400 ]; then - local b_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/b.sh" - if curl -fsSL "$b_url" -o "$b_file" 2>/dev/null; then - echo "$current_time" > "$last_update_file" - . "$b_file" # Load the updated function in current shell context + # Update everything once every 24 hours, or if routes.sh is missing + if [ $((current_time - last_update)) -gt 86400 ] || [ ! -f "$routes_file" ]; then + local bootstrap_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh" + local tmp_bootstrap + tmp_bootstrap="$(mktemp)" + + # Download and run the bootstrap installer to update all CLI files + if curl -fsSL "$bootstrap_url" -o "$tmp_bootstrap" 2>/dev/null || wget -qO "$tmp_bootstrap" "$bootstrap_url" 2>/dev/null; then + if bash "$tmp_bootstrap"; then + echo "$current_time" > "$last_update_file" + fi fi - fi - - # 2. Update routes.sh on every run - local routes_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/routes.sh" - mkdir -p "$routes_dir" - if curl -fsSL "$routes_url" -o "$routes_file" 2>/dev/null || wget -qO "$routes_file" "$routes_url" 2>/dev/null; then - : + rm -f "$tmp_bootstrap" fi if [ ! -f "$routes_file" ]; then @@ -37,7 +35,7 @@ b() { return 1 fi - # 3. Execute the routes file + # Execute the routes file bash "$routes_file" "$@" } # <<< bootstrap-cli b function <<< diff --git a/bootstrap.sh b/bootstrap.sh index 1fd9dcf..aab995b 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -10,17 +10,52 @@ fi # Detect if the script is sourced is_sourced=false -if [ -n "${ZSH_VERSION:-}" ]; then - case $ZSH_EVAL_CONTEXT in - *file*) is_sourced=true ;; - esac -elif [ -n "${BASH_VERSION:-}" ]; then - if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then - is_sourced=true +if [ -n "${BASH_SOURCE[0]:-}" ] && [ "${BASH_SOURCE[0]}" != "$0" ]; then + is_sourced=true +fi + +# Locate or download libraries so that sourced installers can use them +BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}" +_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null || pwd)" + +if [ -f "$_SCRIPT_DIR/lib/common.sh" ]; then + # Dev/local mode: source directly from repo + . "$_SCRIPT_DIR/lib/common.sh" + . "$_SCRIPT_DIR/lib/platform.sh" + . "$_SCRIPT_DIR/lib/shell_config.sh" +elif [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then + # Installed mode: source from bootstrap dir + . "$BOOTSTRAP_DIR/lib/common.sh" + . "$BOOTSTRAP_DIR/lib/platform.sh" + . "$BOOTSTRAP_DIR/lib/shell_config.sh" +else + # Standalone/remote mode: download to a temp directory and source + export BOOTSTRAP_TMP_DIR + BOOTSTRAP_TMP_DIR="$(mktemp -d)" + trap 'rm -rf "$BOOTSTRAP_TMP_DIR"' EXIT + + _BASE_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" + _LIBS=("lib/common.sh" "lib/platform.sh" "lib/shell_config.sh") + for _lib in "${_LIBS[@]}"; do + mkdir -p "$BOOTSTRAP_TMP_DIR/$(dirname "$_lib")" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$_BASE_URL/$_lib" -o "$BOOTSTRAP_TMP_DIR/$_lib" 2>/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -qO "$BOOTSTRAP_TMP_DIR/$_lib" "$_BASE_URL/$_lib" 2>/dev/null + fi + done + + if [ -f "$BOOTSTRAP_TMP_DIR/lib/common.sh" ]; then + . "$BOOTSTRAP_TMP_DIR/lib/common.sh" + . "$BOOTSTRAP_TMP_DIR/lib/platform.sh" + . "$BOOTSTRAP_TMP_DIR/lib/shell_config.sh" + else + echo "Error: Failed to download bootstrap libraries." >&2 + exit 1 fi fi -# Install/update the bootstrap loader and download b.sh & routes.sh +# Install/update the bootstrap loader and download all necessary files install_bootstrap() { local target_files=() [ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc") @@ -29,45 +64,59 @@ install_bootstrap() { local routes_dir="$HOME/.config/bootstrap" mkdir -p "$routes_dir" - # Download b.sh and routes.sh from the repository, with fallback to local files if running inside the repo - echo "Downloading bootstrap scripts..." - local b_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/b.sh" - local routes_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/routes.sh" + # List of all files to download/copy + local files=( + "b.sh" + "routes.sh" + "lib/common.sh" + "lib/platform.sh" + "lib/shell_config.sh" + "commands/help.sh" + "commands/conf.sh" + "commands/uninstall.sh" + ) - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - - if [ -f "$script_dir/b.sh" ] && [ -f "$script_dir/routes.sh" ]; then - echo "Using local files from repository..." - cp "$script_dir/b.sh" "$routes_dir/b.sh" - cp "$script_dir/routes.sh" "$routes_dir/routes.sh" - else - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$b_url" -o "$routes_dir/b.sh" - curl -fsSL "$routes_url" -o "$routes_dir/routes.sh" - elif command -v wget >/dev/null 2>&1; then - wget -qO "$routes_dir/b.sh" "$b_url" - wget -qO "$routes_dir/routes.sh" "$routes_url" - else - echo "Error: Neither curl nor wget is installed." >&2 - exit 1 + if [ -f "$_SCRIPT_DIR/b.sh" ] && [ -f "$_SCRIPT_DIR/routes.sh" ]; then + log_info "Using local files from repository..." + for file in "${files[@]}"; do + mkdir -p "$(dirname "$routes_dir/$file")" + if [ -f "$_SCRIPT_DIR/$file" ]; then + cp "$_SCRIPT_DIR/$file" "$routes_dir/$file" + fi + done + + # Also copy installers if they exist locally + if [ -d "$_SCRIPT_DIR/installers" ]; then + mkdir -p "$routes_dir/installers" + cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/" fi + else + log_info "Downloading bootstrap scripts..." + local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" + for file in "${files[@]}"; do + mkdir -p "$(dirname "$routes_dir/$file")" + local file_url="$base_url/$file" + if command -v curl >/dev/null 2>&1; then + curl -fsSL "$file_url" -o "$routes_dir/$file" + elif command -v wget >/dev/null 2>&1; then + wget -qO "$routes_dir/$file" "$file_url" + else + log_error "Neither curl nor wget is installed." + exit 1 + fi + done fi # Set up shell configuration files for config_file in "${target_files[@]}"; do # 1. Clean up old embedded function block if it exists (from previous setup) - if grep -q "# >>> bootstrap-cli b function >>>" "$config_file" 2>/dev/null; then - sed -i '/# >>> bootstrap-cli b function >>>/,/# <<< bootstrap-cli b function <<>> bootstrap-cli setup >>>" "$config_file" 2>/dev/null; then - sed -i '/# >>> bootstrap-cli setup >>>/,/# <<< bootstrap-cli setup <<> "$config_file" # >>> bootstrap-cli setup >>> @@ -78,29 +127,18 @@ EOF done } -install_bootstrap +# Only execute installation if not sourced (Fix 3) +if [ "$is_sourced" = false ]; then + install_bootstrap -# Load the b function immediately in the current subshell -if [ -f "$HOME/.config/bootstrap/b.sh" ]; then - . "$HOME/.config/bootstrap/b.sh" -fi + # Load the b function immediately in the current subshell + if [ -f "$HOME/.config/bootstrap/b.sh" ]; then + . "$HOME/.config/bootstrap/b.sh" + fi -# Handle sourcing the shell configuration file -if [ "$is_sourced" = true ]; then - if [ -n "${ZSH_VERSION:-}" ] && [ -f "$HOME/.zshrc" ]; then - echo "Sourcing ~/.zshrc..." - . "$HOME/.zshrc" - elif [ -n "${BASH_VERSION:-}" ] && [ -f "$HOME/.bashrc" ]; then - echo "Sourcing ~/.bashrc..." + # Handle sourcing the shell configuration file + if [ -n "${BASH_VERSION:-}" ] && [ -f "$HOME/.bashrc" ]; then + log_info "Sourcing ~/.bashrc..." . "$HOME/.bashrc" fi -else - echo - echo "Bootstrap CLI installed successfully!" - echo "To start using the 'b' command in this terminal session, run:" - if [ -n "${ZSH_VERSION:-}" ] || [ -f "$HOME/.zshrc" ]; then - echo " source ~/.zshrc" - else - echo " source ~/.bashrc" - fi fi diff --git a/commands/conf.sh b/commands/conf.sh new file mode 100644 index 0000000..0732618 --- /dev/null +++ b/commands/conf.sh @@ -0,0 +1,43 @@ +# Command: conf +# Edits configurations in ~/.config/ + +config_name="${1:-}" +if [ -z "$config_name" ]; then + log_error "Usage: b conf [files...]" + exit 1 +fi +shift + +config_dir="" +if [ -d "$HOME/.config/$config_name" ]; then + config_dir="$HOME/.config/$config_name" +else + # Find matching directory case-insensitively using pure Bash + for d in "$HOME/.config"/*; do + if [ -d "$d" ]; then + basename="${d##*/}" + if [[ "${basename,,}" == *"${config_name,,}"* ]]; then + config_dir="$d" + break + fi + fi + done +fi + +if [ -n "$config_dir" ] && [ -d "$config_dir" ]; then + editor="${EDITOR:-nvim}" + log_info "Opening editor in $config_dir" + + # Run editor in a subshell so parent working directory is unchanged + ( + cd "$config_dir" || exit 1 + if [ $# -gt 0 ]; then + "$editor" "$@" + else + "$editor" . + fi + ) +else + log_error "Could not find config directory matching: $config_name" + exit 1 +fi diff --git a/commands/help.sh b/commands/help.sh new file mode 100644 index 0000000..04cdc6f --- /dev/null +++ b/commands/help.sh @@ -0,0 +1,13 @@ +# Command: help +# Lists all available bootstrap commands and installers + +echo "Available bootstrap commands:" +# Non-installers first (aligned to 6 chars width) +printf " %-6s - %s\n" "all" "List all available commands" +printf " %-6s - %s\n" "conf" "Edit config (e.g. b conf nvim)" +printf " %-6s - %s\n" "bye" "Uninstall Bootstrap CLI helper" + +# Installers second +for key in "${INSTALLER_KEYS[@]}"; do + printf " %-6s - %s\n" "$key" "${INSTALLERS[$key]}" +done diff --git a/commands/uninstall.sh b/commands/uninstall.sh new file mode 100644 index 0000000..718316b --- /dev/null +++ b/commands/uninstall.sh @@ -0,0 +1,26 @@ +# Command: uninstall (bye) +# Removes bootstrap CLI and cleans up shell configuration files + +# Source libraries if needed (should already be sourced by routes.sh, but just in case) +if [ -z "${_LIB_SHELL_CONFIG_SOURCED:-}" ]; then + _LIB_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}/lib" + . "$_LIB_DIR/shell_config.sh" +fi + +log_info "Removing bootstrap CLI completely..." + +# Get targets using the library function +IFS=' ' read -ra target_files <<< "$(get_shell_configs)" + +for config_file in "${target_files[@]}"; do + # Remove loader setup block + remove_block "$config_file" "bootstrap-cli setup" + + # Remove old embedded b function block + remove_block "$config_file" "bootstrap-cli b function" +done + +# Remove the installation directory +rm -rf "${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}" + +log_success "Bootstrap CLI removed successfully. (Note: Run 'unset -f b' to clear it from the current session)" diff --git a/installers/install_nvim.sh b/installers/install_nvim.sh index 0808cec..ec8fafc 100644 --- a/installers/install_nvim.sh +++ b/installers/install_nvim.sh @@ -2,18 +2,8 @@ # # Neovim Installer Script # -# What this script does: -# 1. Detects the Linux distribution. -# 2. Installs required dependencies (git, wget, tar, curl, unzip, ripgrep, fd, cmake, make, gcc, python, nodejs, npm, xclip, wl-clipboard, fzf). -# 3. Checks whether Neovim 0.11.7 is already installed. -# 4. Prompts before installing or upgrading Neovim. -# 5. Installs the official Neovim binary to /opt/nvim. -# 6. Creates a symlink at /usr/local/bin/nvim. -# 7. Clones the Neovim configuration into ~/.config/nvim. -# 8. Configures shell files (~/.bashrc / ~/.zshrc) to set alias vim="nvim" and export EDITOR="nvim". -# -# Run metascript to check if the shell is bash +# Run metascript to check if the shell is bash and load libraries PARENT_DIR="$(dirname "$0")/.." METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh" METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh" @@ -33,33 +23,21 @@ fi set -euo pipefail - NVIM_VERSION="0.11.7" -NVIM_URL="https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux-x86_64.tar.gz" NVIM_INSTALL_DIR="/opt/nvim" NVIM_CONFIG_REPO="https://git.adityagupta.dev/sortedcord/editor.git" NVIM_CONFIG_DIR="$HOME/.config/nvim" -TMP_DIR="$(mktemp -d)" - +TMP_DIR="$(make_temp_dir)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT -confirm() { - local prompt="$1" - local response - - read -r -p "$prompt [y/N]: " response /dev/null 2>&1; then - echo "Arch Linux detected" - sudo pacman -Sy --needed git wget tar curl unzip ripgrep fd cmake make gcc python nodejs npm xclip wl-clipboard fzf - - elif command -v apt >/dev/null 2>&1; then - echo "Debian/Ubuntu detected" - sudo apt update - sudo apt install -y git wget tar curl unzip ripgrep fd-find cmake build-essential python3 python3-pip python3-venv nodejs npm xclip wl-clipboard fzf + log_info "Detecting distribution and installing dependencies..." + pkg_install \ + git wget tar curl unzip ripgrep fzf nodejs npm xclip wl-clipboard \ + "arch:fd|debian:fd-find|fedora:fd-find" \ + "arch:cmake|debian:cmake|fedora:cmake" \ + "arch:make|debian:build-essential|fedora:make" \ + "arch:gcc|debian:build-essential|fedora:gcc" \ + "arch:python|debian:python3|fedora:python3" \ + "debian:python3-pip|fedora:python3-pip" \ + "debian:python3-venv" \ + "fedora:gcc-c++" - # Create a symlink for fd-find if it doesn't already exist as fd - if ! command -v fd >/dev/null 2>&1 && command -v fdfind >/dev/null 2>&1; then - echo "Creating symlink for fd..." - sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd - fi - - elif command -v dnf >/dev/null 2>&1; then - echo "Fedora detected" - sudo dnf install -y git wget tar curl unzip ripgrep fd-find cmake make gcc gcc-c++ python3 python3-pip nodejs npm xclip wl-clipboard fzf - - else - echo "Unsupported distribution." - exit 1 - fi + create_fd_symlink } install_nvim() { local current_version="" - if command -v nvim >/dev/null 2>&1; then + if has_command nvim; then current_version="$(nvim --version | head -n1 | awk '{print $2}')" if [[ "$current_version" == "v${NVIM_VERSION}" ]] || [[ "$current_version" == "${NVIM_VERSION}" ]]; then - echo "Neovim ${current_version} already installed." + log_info "Neovim ${current_version} already installed." return fi - echo "Detected Neovim ${current_version}" + log_info "Detected Neovim ${current_version}" if ! confirm "Upgrade to Neovim v${NVIM_VERSION}?"; then - echo "Skipping Neovim upgrade." + log_info "Skipping Neovim upgrade." return fi else - echo "Neovim not installed." + log_info "Neovim not installed." if ! confirm "Install Neovim v${NVIM_VERSION}?"; then - echo "Skipping Neovim installation." + log_info "Skipping Neovim installation." return fi fi - echo "Downloading Neovim v${NVIM_VERSION}..." + # Detect architecture to resolve the release binary name (Fix 4) + local arch + arch=$(detect_arch) + local nvim_arch="" + case "$arch" in + x86_64) nvim_arch="linux-x86_64" ;; + arm64) nvim_arch="linux-arm64" ;; + *) log_error "Unsupported architecture: $arch"; exit 1 ;; + esac - wget -qO "$TMP_DIR/nvim.tar.gz" "$NVIM_URL" + local nvim_url="https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-${nvim_arch}.tar.gz" + + log_info "Downloading Neovim v${NVIM_VERSION} for ${arch}..." + if has_command curl; then + curl -fsSL "$nvim_url" -o "$TMP_DIR/nvim.tar.gz" + else + wget -qO "$TMP_DIR/nvim.tar.gz" "$nvim_url" + fi tar -xzf "$TMP_DIR/nvim.tar.gz" -C "$TMP_DIR" sudo rm -rf "$NVIM_INSTALL_DIR" - sudo mv "$TMP_DIR/nvim-linux-x86_64" "$NVIM_INSTALL_DIR" + sudo mv "$TMP_DIR/nvim-${nvim_arch}" "$NVIM_INSTALL_DIR" sudo ln -sf "$NVIM_INSTALL_DIR/bin/nvim" /usr/local/bin/nvim - echo "Installed:" + log_success "Installed:" nvim --version | head -n1 } @@ -158,34 +139,26 @@ install_config() { rm -rf "$NVIM_CONFIG_DIR" fi - echo "Cloning configuration to $NVIM_CONFIG_DIR..." + log_info "Cloning configuration to $NVIM_CONFIG_DIR..." git clone "$NVIM_CONFIG_REPO" "$NVIM_CONFIG_DIR" - echo "Configuration installed." + log_success "Configuration installed." } configure_shell() { - local target_files=() - [ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc") - [ -f "$HOME/.zshrc" ] && target_files+=("$HOME/.zshrc") + IFS=' ' read -ra target_files <<< "$(get_shell_configs)" for config_file in "${target_files[@]}"; do local modified=false - # Add alias vim=nvim if not present - if ! grep -q "alias vim=" "$config_file" 2>/dev/null; then - echo "Adding alias vim=nvim to $config_file..." - echo 'alias vim="nvim"' >> "$config_file" + if add_alias_if_missing "$config_file" "vim" "nvim"; then modified=true fi - # Add export EDITOR=nvim if not present - if ! grep -q "export EDITOR=" "$config_file" 2>/dev/null; then - echo "Setting EDITOR=nvim in $config_file..." - echo 'export EDITOR="nvim"' >> "$config_file" + if add_env_if_missing "$config_file" "EDITOR" "nvim"; then modified=true fi - # Source if modified + # Source if modified (only for bashrc, and not when sourced to prevent recursion) if [ "$modified" = true ] && [ "$config_file" = "$HOME/.bashrc" ]; then . "$config_file" 2>/dev/null || true fi @@ -200,7 +173,7 @@ main() { configure_shell echo - echo "Installation complete." + log_success "Installation complete." } main "$@" \ No newline at end of file diff --git a/installers/install_yazi.sh b/installers/install_yazi.sh index db6e4f2..961c178 100755 --- a/installers/install_yazi.sh +++ b/installers/install_yazi.sh @@ -2,18 +2,8 @@ # # Yazi Installer Script # -# What this script does: -# 1. Detects the Linux distribution. -# 2. Prompts before installing or upgrading Yazi. -# 3. Installs Yazi: -# * Arch Linux: Installs yazi via pacman. -# * Debian / Ubuntu: Downloads the latest .deb release package from GitHub and installs it. -# * Fedora: Enables the COPR repository (lihaohong/yazi) and installs yazi (initially skipping weak dependencies). -# 4. Subsequently installs the dependencies (ffmpeg, 7zip / p7zip-full, jq, poppler, fd / fd-find, ripgrep, fzf, zoxide, resvg, imagemagick) to make Yazi available quicker. -# 5. Configures a shell wrapper function 'y' in ~/.bashrc and ~/.zshrc that allows changing directory on exit. -# -# Run metascript to check if the shell is bash +# Run metascript to check if the shell is bash and load libraries PARENT_DIR="$(dirname "$0")/.." METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh" METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh" @@ -33,37 +23,17 @@ fi set -euo pipefail -TMP_DIR="$(mktemp -d)" - +TMP_DIR="$(make_temp_dir)" cleanup() { rm -rf "$TMP_DIR" } trap cleanup EXIT -confirm() { - local prompt="$1" - local response - - read -r -p "$prompt [y/N]: " response >> yazi wrapper >>>" "$config_file" 2>/dev/null; then - sed -i '/# >>> yazi wrapper >>>/,/# <<< yazi wrapper <<> "$config_file" - -# >>> yazi wrapper >>> + local wrapper_content + wrapper_content=$(cat << 'EOF' # Shell wrapper for yazi to change directory on exit y() { local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" @@ -73,108 +43,112 @@ y() { fi rm -f -- "$tmp" } -# <<< yazi wrapper <<< EOF +) + + for config_file in "${target_files[@]}"; do + log_info "Adding yazi wrapper function 'y' to $config_file..." + inject_block "$config_file" "yazi wrapper" "$wrapper_content" done # Source ~/.bashrc to make the alias immediately available in the current shell context (if sourced) if [ -f "$HOME/.bashrc" ]; then - echo "Sourcing ~/.bashrc..." - . "$HOME/.bashrc" + log_info "Sourcing ~/.bashrc..." + . "$HOME/.bashrc" 2>/dev/null || true fi } install_yazi() { - echo "Detecting distribution..." + local distro + distro=$(detect_distro) - if command -v pacman >/dev/null 2>&1; then - echo "Arch Linux detected" - if command -v yazi >/dev/null 2>&1; then + if [ "$distro" = "arch" ]; then + log_info "Arch Linux detected" + if has_command yazi; then if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then - echo "Skipping Yazi installation." + log_info "Skipping Yazi installation." return fi else if ! confirm "Install Yazi and its dependencies?"; then - echo "Skipping Yazi installation." + log_info "Skipping Yazi installation." return fi fi - echo "Installing Yazi..." - sudo pacman -Sy --needed yazi + log_info "Installing Yazi..." + pkg_install yazi + log_info "Installing dependencies subsequently..." + pkg_install ffmpeg 7zip jq poppler fd ripgrep fzf zoxide resvg imagemagick - echo "Installing dependencies subsequently..." - sudo pacman -S --needed ffmpeg 7zip jq poppler fd ripgrep fzf zoxide resvg imagemagick - - elif command -v apt >/dev/null 2>&1; then - echo "Debian/Ubuntu detected" - if command -v yazi >/dev/null 2>&1; then + elif [ "$distro" = "debian" ]; then + log_info "Debian/Ubuntu detected" + if has_command yazi; then if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then - echo "Skipping Yazi installation." + log_info "Skipping Yazi installation." return fi else if ! confirm "Install Yazi and its dependencies?"; then - echo "Skipping Yazi installation." + log_info "Skipping Yazi installation." return fi fi - sudo apt update - sudo apt install -y curl wget git + pkg_install curl wget git - echo "Fetching latest Yazi version from GitHub..." - LATEST_TAG=$(curl -sL https://api.github.com/repos/sxyazi/yazi/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') - if [ -z "$LATEST_TAG" ]; then - LATEST_TAG="v26.5.6" + log_info "Fetching latest Yazi version from GitHub..." + local latest_tag + latest_tag=$(curl -sL https://api.github.com/repos/sxyazi/yazi/releases/latest | grep '"tag_name":' | head -n1 | sed -E 's/.*"tag_name": "([^"]+)".*/\1/') + if [ -z "$latest_tag" ]; then + latest_tag="v26.5.6" fi - DEB_URL="https://github.com/sxyazi/yazi/releases/download/${LATEST_TAG}/yazi-x86_64-unknown-linux-gnu.deb" - echo "Downloading Yazi ${LATEST_TAG} from ${DEB_URL}..." - wget -qO "$TMP_DIR/yazi.deb" "$DEB_URL" + local deb_url="https://github.com/sxyazi/yazi/releases/download/${latest_tag}/yazi-x86_64-unknown-linux-gnu.deb" + log_info "Downloading Yazi ${latest_tag} from ${deb_url}..." + if has_command curl; then + curl -fsSL "$deb_url" -o "$TMP_DIR/yazi.deb" + else + wget -qO "$TMP_DIR/yazi.deb" "$deb_url" + fi - echo "Installing Yazi package..." + log_info "Installing Yazi package..." sudo apt install -y "$TMP_DIR/yazi.deb" - echo "Installing dependencies subsequently..." - sudo apt install -y ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick 7zip || \ - sudo apt install -y ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick p7zip-full + log_info "Installing dependencies subsequently..." + pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick 7zip || \ + pkg_install ffmpeg jq poppler-utils fd-find ripgrep fzf zoxide resvg imagemagick p7zip-full - # Create a symlink for fd-find if it doesn't already exist as fd - if ! command -v fd >/dev/null 2>&1 && command -v fdfind >/dev/null 2>&1; then - echo "Creating symlink for fd..." - sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd - fi + create_fd_symlink - elif command -v dnf >/dev/null 2>&1; then - echo "Fedora detected" - if command -v yazi >/dev/null 2>&1; then + elif [ "$distro" = "fedora" ]; then + log_info "Fedora detected" + if has_command yazi; then if ! confirm "Yazi is already installed. Reinstall/Upgrade?"; then - echo "Skipping Yazi installation." + log_info "Skipping Yazi installation." return fi else if ! confirm "Install Yazi and its dependencies?"; then - echo "Skipping Yazi installation." + log_info "Skipping Yazi installation." return fi fi - echo "Installing dnf-plugins-core..." - sudo dnf install -y dnf-plugins-core + log_info "Installing dnf-plugins-core..." + pkg_install dnf-plugins-core - echo "Enabling lihaohong/yazi copr repo..." + log_info "Enabling lihaohong/yazi copr repo..." sudo dnf copr enable -y lihaohong/yazi - echo "Installing Yazi (without weak dependencies first)..." + log_info "Installing Yazi (without weak dependencies first)..." sudo dnf install -y yazi --setopt=install_weak_deps=False - echo "Installing weak dependencies subsequently..." - sudo dnf install -y yazi + log_info "Installing weak dependencies subsequently..." + pkg_install yazi else - echo "Unsupported distribution." + log_error "Unsupported distribution." exit 1 fi } @@ -183,7 +157,7 @@ main() { install_yazi add_y_wrapper echo - echo "Yazi installation and configuration complete." + log_success "Yazi installation and configuration complete." } main "$@" diff --git a/installers/install_zoxide.sh b/installers/install_zoxide.sh index 1bff5d9..7c111c9 100755 --- a/installers/install_zoxide.sh +++ b/installers/install_zoxide.sh @@ -2,16 +2,8 @@ # # Zoxide Installer Script # -# What this script does: -# 1. Detects the Linux distribution. -# 2. Checks whether zoxide is already installed. -# 3. Prompts before installing or upgrading zoxide. -# 4. Downloads and runs the official zoxide installer script. -# 5. Configures shell configuration files (~/.bashrc / ~/.zshrc) to initialize zoxide. -# 6. Checks if fzf is installed; if not, installs it using the system package manager. -# -# Run metascript to check if the shell is bash +# Run metascript to check if the shell is bash and load libraries PARENT_DIR="$(dirname "$0")/.." METASCRIPT_LOCAL="$PARENT_DIR/bootstrap.sh" METASCRIPT_URL="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/bootstrap.sh" @@ -31,94 +23,61 @@ fi set -euo pipefail -confirm() { - local prompt="$1" - local response - - read -r -p "$prompt [y/N]: " response /dev/null 2>&1; then - echo "curl not found. Installing curl..." - if command -v pacman >/dev/null 2>&1; then - sudo pacman -Sy --needed curl - elif command -v apt >/dev/null 2>&1; then - sudo apt update - sudo apt install -y curl - elif command -v dnf >/dev/null 2>&1; then - sudo dnf install -y curl - fi + if ! has_command curl; then + log_info "curl not found. Installing curl..." + pkg_install curl fi } install_fzf() { - if command -v fzf >/dev/null 2>&1; then - echo "fzf is already installed." + if has_command fzf; then + log_info "fzf is already installed." return fi - echo "fzf not found. Installing fzf..." - if command -v pacman >/dev/null 2>&1; then - sudo pacman -Sy --needed fzf - elif command -v apt >/dev/null 2>&1; then - sudo apt update - sudo apt install -y fzf - elif command -v dnf >/dev/null 2>&1; then - sudo dnf install -y fzf - else - echo "Warning: Unsupported distribution. Please install fzf manually." >&2 - fi + log_info "fzf not found. Installing fzf..." + pkg_install fzf } install_zoxide() { - if command -v zoxide >/dev/null 2>&1 || [ -f "$HOME/.local/bin/zoxide" ]; then + if has_command zoxide || [ -f "$HOME/.local/bin/zoxide" ]; then if ! confirm "Zoxide is already installed. Reinstall/Upgrade?"; then - echo "Skipping Zoxide installation." + log_info "Skipping Zoxide installation." return fi else if ! confirm "Install Zoxide?"; then - echo "Skipping Zoxide installation." + log_info "Skipping Zoxide installation." return fi fi install_curl - echo "Downloading and running the official zoxide installer..." + log_info "Downloading and running the official zoxide installer..." curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh } configure_shell() { - local target_files=() - [ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc") - [ -f "$HOME/.zshrc" ] && target_files+=("$HOME/.zshrc") - # Add ~/.local/bin to PATH for the current process export PATH="$HOME/.local/bin:$PATH" - for config_file in "${target_files[@]}"; do - # Clean up old block if it exists - if grep -q "# >>> zoxide init >>>" "$config_file" 2>/dev/null; then - sed -i '/# >>> zoxide init >>>/,/# <<< zoxide init <<> "$config_file" + log_info "Adding zoxide initialization to $config_file..." + local content + content="eval \"\$(zoxide init --cmd cd $shell_name)\"" + + inject_block "$config_file" "zoxide init" "$content" -# >>> zoxide init >>> -eval "\$(zoxide init --cmd cd $shell_name)" -# <<< zoxide init <<< -EOF - # Source if modified (only for bashrc currently) + # Source if modified (only for bashrc) if [ "$config_file" = "$HOME/.bashrc" ]; then . "$config_file" 2>/dev/null || true fi @@ -131,7 +90,7 @@ main() { install_fzf echo - echo "Zoxide installation and configuration complete." + log_success "Zoxide installation and configuration complete." } main "$@" diff --git a/lib/common.sh b/lib/common.sh new file mode 100644 index 0000000..4ce214e --- /dev/null +++ b/lib/common.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# Shared utility functions for bootstrap CLI + +# Avoid double sourcing +if [ -n "${_LIB_COMMON_SOURCED:-}" ]; then + return 0 +fi +_LIB_COMMON_SOURCED=1 + +# Ensure running in Bash +require_bash() { + if [ -z "${BASH_VERSION:-}" ]; then + echo "Error: This script must be run using bash." >&2 + exit 1 + fi +} + +# Color definitions (only if stdout is a TTY) +if [ -t 1 ]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + NC='\033[0m' # No Color +else + RED='' + GREEN='' + YELLOW='' + BLUE='' + NC='' +fi + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARNING]${NC} $1" >&2 +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +# Yes/No Confirmation prompt +confirm() { + local prompt="$1" + local response + + # Read from /dev/tty to support piped installations + read -r -p "$prompt [y/N]: " response /dev/null 2>&1 +} + +# Temporary directory helper with automatic cleanup on exit +make_temp_dir() { + local tmp_dir + tmp_dir="$(mktemp -d)" + echo "$tmp_dir" +} diff --git a/lib/platform.sh b/lib/platform.sh new file mode 100644 index 0000000..549b411 --- /dev/null +++ b/lib/platform.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# Platform and package manager detection for bootstrap CLI + +if [ -n "${_LIB_PLATFORM_SOURCED:-}" ]; then + return 0 +fi +_LIB_PLATFORM_SOURCED=1 + +# Source common utilities if not already loaded +if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then + # Assumes common.sh is in the same directory as platform.sh + # We resolve the directory of the current script + _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + . "$_LIB_DIR/common.sh" +fi + +detect_distro() { + if has_command pacman; then + echo "arch" + elif has_command apt; then + echo "debian" + elif has_command dnf; then + echo "fedora" + else + echo "unknown" + fi +} + +detect_arch() { + local arch + arch="$(uname -m)" + case "$arch" in + x86_64) echo "x86_64" ;; + aarch64|arm64) echo "arm64" ;; + *) echo "$arch" ;; + esac +} + +# Install packages depending on detected distro +# Usage: pkg_install +# Or simpler: map common packages to their distro equivalents +pkg_install() { + local distro + distro=$(detect_distro) + + if [ "$distro" = "unknown" ]; then + log_error "Unsupported distribution. Cannot install packages automatically." + return 1 + fi + + local pkgs=() + for arg in "$@"; do + # Format can be "pkg" or "arch:pkg_a|debian:pkg_d|fedora:pkg_f" + if [[ "$arg" =~ : ]]; then + IFS='|' read -ra PARTS <<< "$arg" + local mapped="" + for part in "${PARTS[@]}"; do + local d_prefix="${part%%:*}" + local d_pkg="${part#*:}" + if [ "$d_prefix" = "$distro" ]; then + mapped="$d_pkg" + break + fi + done + if [ -n "$mapped" ]; then + pkgs+=("$mapped") + fi + else + pkgs+=("$arg") + fi + done + + if [ ${#pkgs[@]} -eq 0 ]; then + return 0 + fi + + log_info "Installing packages via $distro package manager: ${pkgs[*]}" + case "$distro" in + arch) + sudo pacman -Sy --needed --noconfirm "${pkgs[@]}" + ;; + debian) + sudo apt update + sudo apt install -y "${pkgs[@]}" + ;; + fedora) + sudo dnf install -y "${pkgs[@]}" + ;; + esac +} diff --git a/lib/shell_config.sh b/lib/shell_config.sh new file mode 100644 index 0000000..888adee --- /dev/null +++ b/lib/shell_config.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Shell configuration file manipulation utilities for bootstrap CLI + +if [ -n "${_LIB_SHELL_CONFIG_SOURCED:-}" ]; then + return 0 +fi +_LIB_SHELL_CONFIG_SOURCED=1 + +# Source common utilities if not already loaded +if [ -z "${_LIB_COMMON_SOURCED:-}" ]; then + _LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + . "$_LIB_DIR/common.sh" +fi + +# Find existing target shell RC files +get_shell_configs() { + local target_files=() + [ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc") + [ -f "$HOME/.zshrc" ] && target_files+=("$HOME/.zshrc") + echo "${target_files[@]}" +} + +# Remove block from a shell RC file +# Usage: remove_block +remove_block() { + local config_file="$1" + local block_name="$2" + + if [ -f "$config_file" ] && grep -q "# >>> $block_name >>>" "$config_file" 2>/dev/null; then + log_info "Removing block '$block_name' from $config_file" + # We use a temporary file to avoid issues with sed in-place options across BSD/GNU + local tmp_file + tmp_file=$(mktemp) + sed "/# >>> $block_name >>>/,/# <<< $block_name << "$tmp_file" + cat "$tmp_file" > "$config_file" + rm -f "$tmp_file" + fi +} + +# Append block to a shell RC file +# Usage: inject_block +inject_block() { + local config_file="$1" + local block_name="$2" + local content="$3" + + remove_block "$config_file" "$block_name" + + log_info "Adding block '$block_name' to $config_file" + { + echo "" + echo "# >>> $block_name >>>" + echo "$content" + echo "# <<< $block_name <<<" + } >> "$config_file" +} + +# Add alias if not present +# Usage: add_alias_if_missing +add_alias_if_missing() { + local config_file="$1" + local name="$2" + local val="$3" + + if [ -f "$config_file" ] && ! grep -q "alias $name=" "$config_file" 2>/dev/null; then + log_info "Adding alias $name to $config_file" + echo "alias ${name}=\"${val}\"" >> "$config_file" + return 0 # Added + fi + return 1 # Not added (already existed or file doesn't exist) +} + +# Add environment variable if not present +# Usage: add_env_if_missing +add_env_if_missing() { + local config_file="$1" + local name="$2" + local val="$3" + + if [ -f "$config_file" ] && ! grep -q "export $name=" "$config_file" 2>/dev/null; then + log_info "Setting $name in $config_file" + echo "export ${name}=\"${val}\"" >> "$config_file" + return 0 # Added + fi + return 1 # Not added +} + +# Setup fd symlink for Debian/Ubuntu (fdfind -> fd) +create_fd_symlink() { + if ! has_command fd && has_command fdfind; then + log_info "Creating symlink for fd..." + sudo ln -sf "$(command -v fdfind)" /usr/local/bin/fd + fi +} diff --git a/routes.sh b/routes.sh index 6f17a28..c361073 100755 --- a/routes.sh +++ b/routes.sh @@ -1,13 +1,21 @@ #!/usr/bin/env bash - # Central routing script for bootstrap installers. # This file is updated automatically by the 'b' command. -if [ -z "${BASH_VERSION:-}" ]; then - echo "Error: This script must be run using bash." >&2 +BOOTSTRAP_DIR="${BOOTSTRAP_DIR:-$HOME/.config/bootstrap}" + +# Source common library +if [ -f "$BOOTSTRAP_DIR/lib/common.sh" ]; then + . "$BOOTSTRAP_DIR/lib/common.sh" +else + # Fallback/Bootstrap case if lib is not installed yet + echo "Error: Bootstrap libraries not found at $BOOTSTRAP_DIR/lib/" >&2 exit 1 fi +require_bash + +# Registry of installers declare -A INSTALLERS=( [nvim]="Install Neovim 0.11.7 and configuration" [yazi]="Install Yazi terminal file manager and dependencies" @@ -20,7 +28,11 @@ SCRIPT_NAMES="${1:-}" if [ -z "$SCRIPT_NAMES" ] || [ "$SCRIPT_NAMES" = "-h" ] || [ "$SCRIPT_NAMES" = "--help" ]; then SCRIPT_NAMES="all" fi -shift + +# Guard shift (Fix 1) +if [ $# -gt 0 ]; then + shift +fi # Split comma-separated script names IFS=',' read -ra SCRIPTS <<< "$SCRIPT_NAMES" @@ -30,77 +42,53 @@ for script in "${SCRIPTS[@]}"; do if [[ -n "${INSTALLERS[$script]:-}" ]]; then # Capitalize first letter for display (e.g. nvim -> Neovim) display_name="$(echo "${script:0:1}" | tr '[:lower:]' '[:upper:]')${script:1}" - echo "Launching ${display_name} installer..." - curl -fsSL "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/installers/install_${script}.sh" | bash -s -- "$@" + log_info "Launching ${display_name} installer..." + + # Check for local installer first, fallback to curl + local_installer="$BOOTSTRAP_DIR/installers/install_${script}.sh" + if [ -f "$local_installer" ]; then + bash "$local_installer" "$@" + else + if has_command curl; then + curl -fsSL "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/installers/install_${script}.sh" | bash -s -- "$@" + elif has_command wget; then + wget -qO- "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/installers/install_${script}.sh" | bash -s -- "$@" + else + log_error "Neither curl nor wget is installed to download the installer." + exit 1 + fi + fi else # Handle non-installer commands case "$script" in all) - echo "Available bootstrap commands:" - # Non-installers first (aligned to 6 chars width) - printf " %-6s - %s\n" "all" "List all available commands" - printf " %-6s - %s\n" "conf" "Edit config (e.g. b conf nvim)" - printf " %-6s - %s\n" "bye" "Uninstall Bootstrap CLI helper" - # Installers second (iterating procedurally) - for key in "${INSTALLER_KEYS[@]}"; do - printf " %-6s - %s\n" "$key" "${INSTALLERS[$key]}" - done - ;; - conf) - config_name="${1:-}" - if [ -z "$config_name" ]; then - echo "Usage: b conf [files...]" >&2 + if [ -f "$BOOTSTRAP_DIR/commands/help.sh" ]; then + . "$BOOTSTRAP_DIR/commands/help.sh" + else + log_error "Help command script not found." exit 1 fi - shift - - config_dir="" - if [ -d "$HOME/.config/$config_name" ]; then - config_dir="$HOME/.config/$config_name" + ;; + conf) + if [ -f "$BOOTSTRAP_DIR/commands/conf.sh" ]; then + . "$BOOTSTRAP_DIR/commands/conf.sh" "$@" else - config_dir=$(find "$HOME/.config" -mindepth 1 -maxdepth 1 -type d -iname "*$config_name*" 2>/dev/null | head -n1) - fi - - if [ -n "$config_dir" ] && [ -d "$config_dir" ]; then - cd "$config_dir" - editor="${EDITOR:-nvim}" - if [ $# -gt 0 ]; then - $editor "$@" - else - $editor . - fi - else - echo "Could not find that directory" >&2 + log_error "Config editor command script not found." exit 1 fi ;; bye) - echo "Removing bootstrap CLI completely..." - - target_files=() - [ -f "$HOME/.bashrc" ] && target_files+=("$HOME/.bashrc") - [ -f "$HOME/.zshrc" ] && target_files+=("$HOME/.zshrc") - - for config_file in "${target_files[@]}"; do - # Remove loader setup - if grep -q "# >>> bootstrap-cli setup >>>" "$config_file" 2>/dev/null; then - sed -i '/# >>> bootstrap-cli setup >>>/,/# <<< bootstrap-cli setup <<>> bootstrap-cli b function >>>" "$config_file" 2>/dev/null; then - sed -i '/# >>> bootstrap-cli b function >>>/,/# <<< bootstrap-cli b function <<&2 - echo "Run 'b all' to list all available commands." >&2 + log_error "Unknown command '$script'." + log_info "Run 'b all' to list all available commands." exit 1 ;; esac