From b3435768ae0a1d87b1e1ab1eb91d359e09949541 Mon Sep 17 00:00:00 2001 From: seb meiffren Date: Wed, 6 May 2026 08:04:53 +0200 Subject: [PATCH] Add MacOS dotfiles validation support Introduce a Homebrew-backed MacOS installer and validation workflow while preserving the Ubuntu installer path. Co-authored-by: Cursor --- .github/workflows/macos-validation.yml | 75 +++++ Brewfile | 36 +++ README.md | 84 +++++- github-cli/install.sh | 25 +- github-cli/test.sh | 80 ++--- gitlab-cli/install.sh | 28 +- gitlab-cli/test.sh | 9 + hyfetch/install.sh | 66 ++++- hyfetch/test.sh | 15 + install.sh | 390 ++----------------------- install_macos.sh | 286 ++++++++++++++++++ install_ubuntu.sh | 321 ++++++++++++++++++++ kitty/install.sh | 12 +- kitty/test.sh | 8 + kubectl/.kube/config | 2 +- kubectl/.stow-local-ignore | 5 +- kubectl/install.sh | 99 ++++--- kubectl/test.sh | 61 ++-- nvm/install.sh | 102 +++++-- nvm/test.sh | 16 + scripts/test-lib.sh | 48 +++ test-macos.sh | 158 ++++++++++ vim/install.sh | 52 +++- vim/test.sh | 19 ++ zsh/.zshrc | 41 ++- zsh/install.sh | 84 ++++-- zsh/test.sh | 18 ++ 27 files changed, 1566 insertions(+), 574 deletions(-) create mode 100644 .github/workflows/macos-validation.yml create mode 100644 Brewfile create mode 100755 install_macos.sh create mode 100755 install_ubuntu.sh create mode 100644 scripts/test-lib.sh create mode 100755 test-macos.sh diff --git a/.github/workflows/macos-validation.yml b/.github/workflows/macos-validation.yml new file mode 100644 index 0000000..9710402 --- /dev/null +++ b/.github/workflows/macos-validation.yml @@ -0,0 +1,75 @@ +name: MacOS validation + +on: + pull_request: + push: + branches: + - macos-support-validation + +jobs: + macos: + name: Validate MacOS support + runs-on: macos-14 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Prepare scripts + run: | + chmod +x install.sh install_macos.sh install_ubuntu.sh test-macos.sh + + - name: Validate shell syntax + run: | + bash -n install.sh install_macos.sh install_ubuntu.sh test-macos.sh + bash -n zsh/install.sh hyfetch/install.sh vim/install.sh kitty/install.sh + bash -n kubectl/install.sh github-cli/install.sh gitlab-cli/install.sh nvm/install.sh + + - name: Validate Brewfile + run: | + ruby -c Brewfile + brew bundle list --file=Brewfile + + - name: Install CI validation dependencies + run: brew install stow + + - name: Run MacOS validation script + run: ./test-macos.sh + + - name: Run safe stow dry-runs + run: | + set -euo pipefail + + TEST_HOME="$(mktemp -d)" + mkdir -p "$TEST_HOME/.config" + + dry_run_home_module() { + local module="$1" + echo "Dry-running home stow for ${module}" + ( + cd "$module" + stow -n -v -t "$TEST_HOME" . + ) + } + + dry_run_config_module() { + local module="$1" + echo "Dry-running .config stow for ${module}" + ( + cd "$module" + stow -n -v -t "$TEST_HOME/.config" .config + ) + } + + dry_run_home_module zsh + dry_run_home_module nvm + dry_run_config_module hyfetch + dry_run_config_module vim + dry_run_config_module kitty + dry_run_config_module kubectl + dry_run_home_module kubectl + dry_run_config_module github-cli + dry_run_config_module gitlab-cli + + - name: Verify MacOS installer help + run: bash install_macos.sh --help diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..c211430 --- /dev/null +++ b/Brewfile @@ -0,0 +1,36 @@ +tap "homebrew/bundle" +tap "int128/kubelogin" + +# Core tools used by the dotfiles installers +brew "stow" +brew "git" +brew "curl" +brew "wget" +brew "unzip" + +# Shell and terminal workflow +brew "zsh" +brew "fzf" + +# Editors and search tools +brew "neovim" +brew "ripgrep" +brew "fd" + +# DevOps and cloud tooling +brew "kubectl" +brew "int128/kubelogin/kubelogin" +brew "gh" +brew "glab" +brew "nvm" + +# Optional CLI tools used by modules +brew "hyfetch" +brew "mas" + +# Desktop applications +cask "kitty" +cask "docker" +cask "slack" +cask "powershell" +cask "font-hack-nerd-font" diff --git a/README.md b/README.md index ed673df..b4046c5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# Ubuntu 24 LTS Dotfiles +# Cross-Platform Dotfiles -This repository contains configuration files and setup scripts for Ubuntu 24 LTS. It uses GNU Stow for managing dotfiles and provides a streamlined way to set up a development environment. +This repository contains configuration files and setup scripts for Ubuntu 24 LTS and MacOS. It uses GNU Stow for managing dotfiles and provides a streamlined way to set up a development environment. ## Overview -This project aims to provide a consistent and reproducible setup for Ubuntu 24 LTS systems, focusing on development tools and system configurations. The setup is managed through GNU Stow, which creates symbolic links to the appropriate locations in the home directory. +This project aims to provide a consistent and reproducible setup for Ubuntu 24 LTS and MacOS systems, focusing on development tools and system configurations. The setup is managed through GNU Stow, which creates symbolic links to the appropriate locations in the home directory. ## Manual Setup Steps @@ -54,9 +54,9 @@ For Framework Laptop 13 (AMD Ryzen™ AI 300 Series), update the firmware using - Keep the laptop plugged in throughout the entire process - Do not close the lid during the update process -### System Preparation +### Ubuntu System Preparation -After completing the manual steps above: +After completing the manual steps above on Ubuntu: 1. **Update the system:** ```bash @@ -74,11 +74,34 @@ After completing the manual steps above: cd dotfiles ``` +### MacOS System Preparation + +On MacOS, the installer uses Homebrew and a `Brewfile` for package management: + +1. **Install Xcode command line tools:** + ```bash + xcode-select --install + ``` + +2. **Clone this repository:** + ```bash + git clone + cd dotfiles + ``` + +3. **Run the installer:** + ```bash + ./install.sh --all + ``` + +If Homebrew is missing, the MacOS installer will bootstrap it from the official installer before running `brew bundle`. + ## Prerequisites -- Ubuntu 24 LTS +- Ubuntu 24 LTS or MacOS - GNU Stow - Basic development tools +- Homebrew on MacOS - Framework Laptop firmware updated (if applicable) ## Project Structure @@ -89,8 +112,12 @@ Each directory in this repository represents a specific tool or configuration se . ├── README.md # This file ├── CHANGELOG.md # Project evolution and changes -├── install.sh # Main installation script +├── install.sh # OS dispatcher for Ubuntu and MacOS +├── install_ubuntu.sh # Ubuntu installation script +├── install_macos.sh # MacOS installation script +├── Brewfile # Homebrew bundle for MacOS packages ├── test-all.sh # Test script for all configurations +├── test-macos.sh # Lightweight MacOS validation script ├── test-docker.sh # Script to test in Docker environment ├── Dockerfile # Docker configuration for testing ├── docker-compose.yml # Docker Compose configuration @@ -103,6 +130,10 @@ Each directory in this repository represents a specific tool or configuration se ## Installation +The root `install.sh` detects the operating system and dispatches to: +- `install_ubuntu.sh` on Linux/Ubuntu +- `install_macos.sh` on MacOS + ### Interactive Installation To install configurations interactively: @@ -113,13 +144,12 @@ To install configurations interactively: The installation process includes: 1. Checking and installing required dependencies -2. Managing existing `.config` directory: - - If `.config` exists, you'll be prompted to: - - Create a backup (recommended) - backups are stored in the `backup` directory - - Overwrite existing configuration - - Exit installation - - If `.config` doesn't exist, it will be created -3. Installing selected configurations with proper backups +2. Ensuring the repository backup directory and `$HOME/.config` exist +3. Installing selected configurations with module-level backups where the module supports them + +The root installers do not move or overwrite an existing `$HOME/.config` directory. Existing module configuration is handled by each module installer before it calls Stow; modules may back up, remove a repository-managed symlink, or fail with a clear conflict that requires manual review. + +On MacOS, package installation is handled by Homebrew through `Brewfile`. Linux-only modules are skipped by the MacOS installer. ### Update System Packages @@ -145,7 +175,7 @@ To install all configurations automatically in a specific order: ./install.sh --all ``` -This will install all modules in the following predefined order: +On Ubuntu, this will install all modules in the following predefined order: 1. **apt-packages** - System packages and WireGuard VPN 2. **certs** - SSL/TLS certificates 3. **zsh** - Enhanced shell configuration @@ -161,6 +191,20 @@ This will install all modules in the following predefined order: 13. **nvm** - Node Version Manager 14. **gitlab-cli** - GitLab command-line interface +On MacOS, this will install Homebrew packages from `Brewfile` and then install these shared modules: +1. **zsh** - Enhanced shell configuration +2. **hyfetch** - System information display +3. **vim** - Neovim text editor +4. **kitty** - Terminal emulator +5. **kubectl** - Kubernetes CLI tools +6. **github-cli** - GitHub command-line interface +7. **gitlab-cli** - GitLab command-line interface +8. **nvm** - Node Version Manager + +The MacOS installer intentionally skips Linux-specific modules: `apt-packages`, `snap-config`, `flatpak-config`, `appimaged`, `certs`, `docker`, `slack`, and `powershell`. + +For MacOS, `Brewfile` installs app casks such as Docker Desktop, Slack, Kitty, and PowerShell. Those casks only install the applications; they do not run the Linux module configuration scripts. Add a dedicated MacOS module later if an app needs Mac-specific configuration beyond installation. + ### Individual Module Installation To install specific tool configurations: @@ -171,7 +215,7 @@ stow [tool_name] Note: Each module's installation script will: 1. Check for existing configuration -2. Create a backup if needed +2. Create a backup if implemented by that module 3. Install any missing dependencies 4. Apply the configuration using stow @@ -184,6 +228,14 @@ To test all configurations locally: ./test-all.sh ``` +To run the lightweight MacOS validation checks: + +```bash +./test-macos.sh +``` + +This validates MacOS installer syntax, the `Brewfile`, and the expected shared module layout. When run on MacOS with Homebrew available, it also asks `brew bundle` to validate the bundle file. + ### Docker Testing To test the configuration in a Docker environment: diff --git a/github-cli/install.sh b/github-cli/install.sh index 2ab1aef..f6266c0 100755 --- a/github-cli/install.sh +++ b/github-cli/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="github-cli" +OS_TYPE="$(uname -s)" # Catppuccin Mocha color scheme # Base colors @@ -61,29 +62,39 @@ package_installed() { # Function to install GitHub CLI via apt install_github_cli() { + if [ "$OS_TYPE" = "Darwin" ]; then + print_status "Installing GitHub CLI via Homebrew..." + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install GitHub CLI on MacOS" + exit 1 + fi + brew install gh + return 0 + fi + print_status "Installing GitHub CLI via apt..." - + # Add GitHub CLI repository if not already added if [ ! -f "/etc/apt/sources.list.d/github-cli.list" ]; then print_status "Adding GitHub CLI repository..." - + # Download and install the signing key curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg - + # Add the repository echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null - + # Update package list print_status "Updating package list..." sudo apt-get update else print_status "GitHub CLI repository already configured" fi - + # Install GitHub CLI print_status "Installing GitHub CLI..." sudo apt-get install -y gh - + # Verify installation if command -v gh &> /dev/null; then print_success "GitHub CLI installed successfully!" @@ -145,4 +156,4 @@ print_success "$MODULE_NAME configuration installed successfully!" print_status "Next steps:" print_status "1. Run 'gh auth login' to authenticate with GitHub" print_status "2. Configure your Git username and email if not already set" -print_status "3. Test the installation with 'gh --help'" \ No newline at end of file +print_status "3. Test the installation with 'gh --help'" \ No newline at end of file diff --git a/github-cli/test.sh b/github-cli/test.sh index 13d30e8..90550bf 100755 --- a/github-cli/test.sh +++ b/github-cli/test.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" MODULE_NAME="github-cli" CONFIG_DIR="$HOME/.config/gh" +OS_TYPE="$(uname -s)" # Catppuccin Mocha color scheme # Base colors @@ -49,35 +50,15 @@ print_header() { echo -e "\n${MAUVE}=== $1 ===${BASE}\n" } +# shellcheck source=../scripts/test-lib.sh +. "$SCRIPT_DIR/../scripts/test-lib.sh" + # Test functions test_stow_link() { local target="$1" local source="$2" - - print_status "Testing stow link: $target" - - # Check if the target is a symbolic link - if [ ! -L "$target" ]; then - print_error "$target is not a symbolic link" - return 1 - fi - - # Get the absolute path of the source - local abs_source="$(cd "$(dirname "$source")" && pwd)/$(basename "$source")" - - # Get the absolute path of the target's link - local abs_target="$(readlink -f "$target")" - - # Compare the paths - if [ "$abs_target" = "$abs_source" ]; then - print_success "$target is properly linked by stow" - return 0 - else - print_error "$target is not properly linked by stow" - print_error "Expected: $abs_source" - print_error "Got: $abs_target" - return 1 - fi + + test_stow_link_portable "$target" "$source" } test_file_exists() { @@ -110,8 +91,15 @@ test_file_permissions() { local file="$1" local expected_perms="$2" print_status "Testing file permissions: $file" - - local actual_perms=$(stat -c "%a" "$file") + + local actual_perms + file="$(resolve_path_portable "$file")" + + if [ "$OS_TYPE" = "Darwin" ]; then + actual_perms=$(stat -f "%Lp" "$file") + else + actual_perms=$(stat -c "%a" "$file") + fi if [ "$actual_perms" = "$expected_perms" ]; then print_success "File permissions correct: $file ($expected_perms)" return 0 @@ -139,14 +127,19 @@ test_dependency() { test_package_installed() { local package="$1" print_status "Testing package installation: $package" - - if dpkg -l "$package" &>/dev/null; then + + if [ "$OS_TYPE" = "Darwin" ]; then + if brew list "$package" >/dev/null 2>&1; then + print_success "Package installed: $package" + return 0 + fi + elif dpkg -l "$package" &>/dev/null; then print_success "Package installed: $package" return 0 - else - print_error "Package not installed: $package" - return 1 fi + + print_error "Package not installed: $package" + return 1 } test_github_cli_version() { @@ -176,7 +169,12 @@ test_github_cli_help() { test_apt_repository() { print_status "Testing GitHub CLI apt repository" - + + if [ "$OS_TYPE" != "Linux" ]; then + print_warning "APT repository check is Linux-only, skipping" + return 0 + fi + if [ -f "/etc/apt/sources.list.d/github-cli.list" ]; then print_success "GitHub CLI apt repository configured" return 0 @@ -189,6 +187,15 @@ test_apt_repository() { # Main test execution print_header "Testing $MODULE_NAME configuration" +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + # shellcheck source=../scripts/test-lib.sh + . "$SCRIPT_DIR/../scripts/test-lib.sh" + test_stow_link_portable "$CONFIG_DIR" "$SCRIPT_DIR/.config/gh" + test_file_exists "$CONFIG_DIR/config.yml" + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test dependencies test_dependency "gh" test_dependency "curl" @@ -206,7 +213,12 @@ test_directory_exists "$CONFIG_DIR" # Test stow links test_stow_link "$CONFIG_DIR/config.yml" "$SCRIPT_DIR/.config/gh/config.yml" -test_stow_link "$CONFIG_DIR/hosts.yml" "$SCRIPT_DIR/.config/gh/hosts.yml" + +if [ -f "$SCRIPT_DIR/.config/gh/hosts.yml" ]; then + test_stow_link "$CONFIG_DIR/hosts.yml" "$SCRIPT_DIR/.config/gh/hosts.yml" +else + print_warning "hosts.yml is not managed by this module; skipping stow link check" +fi # Test configuration files if they exist if [ -f "$CONFIG_DIR/config.yml" ]; then diff --git a/gitlab-cli/install.sh b/gitlab-cli/install.sh index a2f5540..17d0687 100755 --- a/gitlab-cli/install.sh +++ b/gitlab-cli/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="gitlab-cli" +OS_TYPE="$(uname -s)" # Catppuccin Mocha color scheme # Base colors @@ -67,13 +68,22 @@ if command_exists glab; then print_success "GitLab CLI (glab) is already installed" glab version else - print_status "Installing GitLab CLI (glab) via snap..." - - if ! sudo snap install glab; then - print_error "Failed to install GitLab CLI via snap" - exit 1 + if [ "$OS_TYPE" = "Darwin" ]; then + print_status "Installing GitLab CLI (glab) via Homebrew..." + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install GitLab CLI on MacOS" + exit 1 + fi + brew install glab + else + print_status "Installing GitLab CLI (glab) via snap..." + + if ! sudo snap install glab; then + print_error "Failed to install GitLab CLI via snap" + exit 1 + fi fi - + # Verify installation if command_exists glab; then print_success "GitLab CLI installed successfully" @@ -87,16 +97,16 @@ fi # Check if configuration directory exists if [ -d "$HOME/.config/glab" ]; then print_warning "Existing GitLab CLI configuration found" - + # Create backup TIMESTAMP=$(date +"%Y-%m-%d_%H-%M") BACKUP_PATH="$BACKUP_DIR/modules/$MODULE_NAME/$TIMESTAMP" mkdir -p "$BACKUP_PATH" - + print_status "Creating backup of existing configuration..." cp -r "$HOME/.config/glab" "$BACKUP_PATH/" print_success "Backup created at: $BACKUP_PATH" - + # Remove existing configuration rm -rf "$HOME/.config/glab" print_status "Removed existing configuration" diff --git a/gitlab-cli/test.sh b/gitlab-cli/test.sh index 340c382..c5ed252 100755 --- a/gitlab-cli/test.sh +++ b/gitlab-cli/test.sh @@ -109,6 +109,15 @@ test_dependency() { # Main test execution print_header "Testing $MODULE_NAME configuration" +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + # shellcheck source=../scripts/test-lib.sh + . "$SCRIPT_DIR/../scripts/test-lib.sh" + test_stow_link_portable "$CONFIG_DIR" "$SCRIPT_DIR/.config/glab" + test_file_exists "$CONFIG_DIR/config.yml" + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test stow links test_stow_link "$CONFIG_DIR" "$SCRIPT_DIR/.config/glab" diff --git a/hyfetch/install.sh b/hyfetch/install.sh index 1a648e1..8085825 100755 --- a/hyfetch/install.sh +++ b/hyfetch/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="hyfetch" +OS_TYPE="$(uname -s)" HYFETCH_CONFIG="$HOME/.config/hyfetch.json" NEOWOFETCH_CONFIG="$HOME/.config/neowofetch/config.conf" LEGACY_NEOFETCH_CONFIG="$HOME/.config/neofetch/config.conf" @@ -67,28 +68,71 @@ mkdir -p "$BACKUP_DIR" # Install HyFetch if not already installed if ! command -v hyfetch &> /dev/null; then print_status "Installing hyfetch..." - sudo apt-get update - sudo apt-get install -y hyfetch + if [ "$OS_TYPE" = "Darwin" ]; then + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install hyfetch on MacOS" + exit 1 + fi + brew install hyfetch + else + sudo apt-get update + sudo apt-get install -y hyfetch + fi print_success "hyfetch installed successfully" else print_status "hyfetch is already installed" fi # Install Hack Nerd Font if not already installed -if ! fc-list | grep -i "Hack Nerd Font" &> /dev/null; then +if [ "$OS_TYPE" = "Darwin" ]; then + font_installed() { + system_profiler SPFontsDataType 2>/dev/null | grep -i "Hack Nerd Font" &> /dev/null + } +else + font_installed() { + fc-list | grep -i "Hack Nerd Font" &> /dev/null + } +fi + +if ! font_installed; then print_status "Installing Hack Nerd Font..." - FONT_DIR="$HOME/.local/share/fonts" - mkdir -p "$FONT_DIR" - wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.1.1/Hack.zip -O /tmp/hack.zip - unzip -q /tmp/hack.zip -d /tmp/hack - cp /tmp/hack/*.ttf "$FONT_DIR" - rm -rf /tmp/hack /tmp/hack.zip - fc-cache -f -v + if [ "$OS_TYPE" = "Darwin" ]; then + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install fonts on MacOS" + exit 1 + fi + brew install --cask font-hack-nerd-font + else + FONT_DIR="$HOME/.local/share/fonts" + mkdir -p "$FONT_DIR" + wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.1.1/Hack.zip -O /tmp/hack.zip + unzip -q /tmp/hack.zip -d /tmp/hack + cp /tmp/hack/*.ttf "$FONT_DIR" + rm -rf /tmp/hack /tmp/hack.zip + fc-cache -f -v + fi print_success "Hack Nerd Font installed successfully" else print_status "Hack Nerd Font is already installed" fi +resolve_link_target() { + local link_path="$1" + local link_target + link_target="$(readlink "$link_path" || true)" + + case "$link_target" in + /*) printf '%s\n' "$link_target" ;; + *) + ( + cd "$(dirname "$link_path")" + cd "$(dirname "$link_target")" + printf '%s/%s\n' "$(pwd -P)" "$(basename "$link_target")" + ) + ;; + esac +} + backup_existing_config() { local config_path="$1" local backup_name="$2" @@ -96,7 +140,7 @@ backup_existing_config() { if [ -e "$config_path" ] || [ -L "$config_path" ]; then if [ -L "$config_path" ]; then local symlink_target - symlink_target="$(readlink -m "$config_path")" + symlink_target="$(resolve_link_target "$config_path")" case "$symlink_target" in "$SCRIPT_DIR"/*) diff --git a/hyfetch/test.sh b/hyfetch/test.sh index 4c048ee..923c9db 100755 --- a/hyfetch/test.sh +++ b/hyfetch/test.sh @@ -80,6 +80,21 @@ test_stow_link() { print_header "Testing $MODULE_NAME configuration" TEST_FAILURES=0 +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + # shellcheck source=../scripts/test-lib.sh + . "$SCRIPT_DIR/../scripts/test-lib.sh" + test_stow_link_portable "$HOME/.config/hyfetch.json" "$SCRIPT_DIR/.config/hyfetch.json" || TEST_FAILURES=$((TEST_FAILURES + 1)) + test_stow_link_portable "$HOME/.config/neowofetch" "$SCRIPT_DIR/.config/neowofetch" || TEST_FAILURES=$((TEST_FAILURES + 1)) + + if [ "$TEST_FAILURES" -gt 0 ]; then + print_error "$TEST_FAILURES test(s) failed" + exit 1 + fi + + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test HyFetch installation if command -v hyfetch &> /dev/null; then print_success "hyfetch is installed" diff --git a/install.sh b/install.sh index b7ad40d..8f458ad 100755 --- a/install.sh +++ b/install.sh @@ -3,378 +3,36 @@ # Exit on any error set -e -# Script directory -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -BACKUP_DIR="$SCRIPT_DIR/backup" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -# Dependencies -declare -a REQUIRED_APT_PACKAGES=( - "stow" - "git" - "wget" - "unzip" - "fontconfig" - "curl" - "libfuse2" -) - -declare -a REQUIRED_SNAP_PACKAGES=( - # Add snap packages here as needed -) - -# Catppuccin Mocha color scheme -# Base colors BASE="\033[0m" -TEXT="\033[38;2;205;214;244m" # Text -SUBTEXT="\033[38;2;166;173;200m" # Subtext -OVERLAY="\033[38;2;108;112;134m" # Overlay -SURFACE="\033[38;2;49;50;68m" # Surface -BASE_COLOR="\033[38;2;30;30;46m" # Base -MANTLE="\033[38;2;24;24;37m" # Mantle -CRUST="\033[38;2;17;17;27m" # Crust +RED="\033[38;2;243;139;168m" +BLUE="\033[38;2;137;180;250m" +GREEN="\033[38;2;166;227;161m" -# Accent colors -RED="\033[38;2;243;139;168m" # Red -GREEN="\033[38;2;166;227;161m" # Green -YELLOW="\033[38;2;249;226;175m" # Yellow -BLUE="\033[38;2;137;180;250m" # Blue -PINK="\033[38;2;245;194;231m" # Pink -MAUVE="\033[38;2;203;166;247m" # Mauve -TEAL="\033[38;2;148;226;213m" # Teal - -# Function to print with colors -print_text() { - echo -e "${TEXT}$1${BASE}" -} - -print_subtext() { - echo -e "${SUBTEXT}$1${BASE}" +print_info() { + echo -e "${BLUE}[i]${BASE} $1" } print_success() { - echo -e "${GREEN}[✓] $1${BASE}" + echo -e "${GREEN}[✓]${BASE} $1" } print_error() { - echo -e "${RED}[✗] $1${BASE}" -} - -print_warning() { - echo -e "${YELLOW}[!] $1${BASE}" -} - -print_info() { - echo -e "${BLUE}[i] $1${BASE}" -} - -print_header() { - echo -e "\n${MAUVE}=== $1 ===${BASE}\n" -} - -# Function to check if a command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Function to install a package using apt -install_apt() { - local package=$1 - print_info "Installing $package via apt..." - sudo apt-get update - sudo apt-get install -y "$package" - print_success "$package installed successfully" -} - -# Function to install a package using snap -install_snap() { - local package=$1 - print_info "Installing $package via snap..." - sudo snap install "$package" - print_success "$package installed successfully" -} - -# Function to update all apt packages -update_apt_packages() { - print_header "Updating APT Packages" - print_info "Updating package lists..." - sudo apt-get update - print_success "Package lists updated" - - print_info "Upgrading all installed packages..." - sudo apt-get upgrade -y - print_success "All packages upgraded" - - print_info "Performing full system upgrade (if available)..." - sudo apt-get full-upgrade -y - print_success "Full system upgrade completed" - - print_info "Cleaning up unused packages..." - sudo apt-get autoremove -y - sudo apt-get autoclean - print_success "Cleanup completed" -} - -# Function to update all snap packages -update_snap_packages() { - print_header "Updating Snap Packages" - print_info "Refreshing all snap packages..." - sudo snap refresh - print_success "All snap packages refreshed" -} - -# Function to update all flatpak packages -update_flatpak_packages() { - print_header "Updating Flatpak Packages" - - if ! command_exists flatpak; then - print_warning "flatpak is not installed, skipping Flatpak updates" - return 0 - fi - - print_info "Updating all system Flatpak packages..." - sudo flatpak update --system -y - print_success "All system Flatpak packages updated" -} - -# Function to update all packages (apt, snap, and flatpak) -update_all_packages() { - print_header "Updating All System Packages" - update_apt_packages - update_snap_packages - update_flatpak_packages - print_success "All packages updated successfully!" -} - -# Function to show help -show_help() { - print_header "Dotfiles Installation Help" - echo -e "${TEXT}Usage:${BASE}" - echo -e " ${BLUE}./install.sh${BASE} [options]" - echo - echo -e "${TEXT}Options:${BASE}" - echo -e " ${GREEN}--all${BASE} Install all modules in predefined order without prompts" - echo -e " ${GREEN}--backup${BASE} Only handle backup of existing .config" - echo -e " ${GREEN}--update${BASE} Update all apt, snap, and flatpak packages" - echo -e " ${GREEN}--help${BASE} Show this help message" - echo - echo -e "${TEXT}Installation Order (--all):${BASE}" - echo -e " ${BLUE}1.${BASE} apt-packages # System packages and WireGuard VPN" - echo -e " ${BLUE}2.${BASE} certs # SSL/TLS certificates" - echo -e " ${BLUE}3.${BASE} zsh # Enhanced shell configuration" - echo -e " ${BLUE}4.${BASE} hyfetch # System information display" - echo -e " ${BLUE}5.${BASE} snap-config # Snap package management" - echo -e " ${BLUE}6.${BASE} flatpak-config # Flatpak package management and Teams" - echo -e " ${BLUE}7.${BASE} vim # Neovim text editor" - echo -e " ${BLUE}8.${BASE} kitty # Terminal emulator" - echo -e " ${BLUE}9.${BASE} kubectl # Kubernetes CLI tools" - echo -e " ${BLUE}10.${BASE} github-cli # GitHub command-line interface" - echo -e " ${BLUE}11.${BASE} slack # Slack desktop application" - echo -e " ${BLUE}12.${BASE} docker # Docker configuration" - echo -e " ${BLUE}13.${BASE} nvm # Node Version Manager" - echo -e " ${BLUE}14.${BASE} gitlab-cli # GitLab command-line interface" - echo - echo -e "${TEXT}Examples:${BASE}" - echo -e " ${BLUE}./install.sh${BASE} # Interactive installation" - echo -e " ${BLUE}./install.sh --all${BASE} # Install everything in order" - echo -e " ${BLUE}./install.sh --backup${BASE} # Only handle backup" - echo -e " ${BLUE}./install.sh --update${BASE} # Update all apt, snap, and flatpak packages" - exit 0 -} - -# Function to ensure backup directory exists -ensure_backup_dir() { - if [ ! -d "$BACKUP_DIR" ]; then - mkdir -p "$BACKUP_DIR" - print_success "Created backup directory at $BACKUP_DIR" - fi -} - -# Function to handle .config directory -handle_config_directory() { - local config_path="$HOME/.config" - - if [ -d "$config_path" ]; then - print_warning "Found existing .config directory. No changes will be made to it by the main installer." - else - print_info "No existing .config directory found" - mkdir -p "$config_path" - print_success "Created new .config directory" - fi -} - -# Function to display and handle menu -show_menu() { - print_header "Dotfiles Installation Menu" - echo -e "${TEXT}1) ${GREEN}Install all modules${BASE}" - echo -e "${TEXT}2) ${BLUE}Select modules to install${BASE}" - echo -e "${TEXT}3) ${RED}Exit${BASE}" - - read -p "$(echo -e "${SUBTEXT}Choose an option [1-3]: ${BASE}")" choice - - case $choice in - 1) - install_all_modules - ;; - 2) - select_modules - ;; - 3) - print_info "Exiting..." - exit 0 - ;; - *) - print_error "Invalid option. Exiting..." - exit 1 - ;; - esac -} - -# Function to install all modules in specific order -install_all_modules() { - print_header "Installing All Modules" - - # Define installation order - declare -a MODULE_ORDER=( - "apt-packages" - "certs" - "zsh" - "hyfetch" - "snap-config" - "flatpak-config" - "vim" - "kitty" - "kubectl" - "github-cli" - "slack" - "docker" - "nvm" - "gitlab-cli" - ) - - # Install modules in the specified order - for module in "${MODULE_ORDER[@]}"; do - if [ -d "$module" ]; then - install_module "$module/" - else - print_warning "Module $module not found, skipping..." - fi - done -} - -# Function to select specific modules -select_modules() { - print_header "Available Modules" - local i=1 - local modules=() - - for dir in */; do - if [ "$dir" != ".cursor/" ] && [ "$dir" != "backup/" ] && [ -d "$dir" ]; then - echo -e "${TEXT}$i) ${BLUE}${dir%/}${BASE}" - modules+=("$dir") - ((i++)) - fi - done - - echo -e "\n${TEXT}$i) ${GREEN}Install selected${BASE}" - echo -e "${TEXT}$((i+1))) ${RED}Cancel${BASE}" - - local selected=() - while true; do - read -p "$(echo -e "${SUBTEXT}Select a module number (or $i to install, $((i+1)) to cancel): ${BASE}")" choice - - if [ "$choice" -eq "$i" ]; then - break - elif [ "$choice" -eq "$((i+1))" ]; then - print_info "Installation cancelled" - exit 0 - elif [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then - selected+=("${modules[$((choice-1))]}") - print_success "Module ${modules[$((choice-1))]} selected" - else - print_error "Invalid option" - fi - done - - print_header "Installing Selected Modules" - for module in "${selected[@]}"; do - install_module "$module" - done -} - -# Function to install a module -install_module() { - local module_dir=$1 - local module_name=$(basename "$module_dir") - - print_info "Installing $module_name configuration..." - - if [ -f "$module_dir/install.sh" ]; then - cd "$module_dir" - ./install.sh - cd "$SCRIPT_DIR" - print_success "$module_name installed successfully" - else - print_warning "No install.sh found in $module_name" - fi -} - -# Function to check and install required tools -check_and_install_tools() { - print_header "Checking Required Tools" - - # Check and install apt packages - for package in "${REQUIRED_APT_PACKAGES[@]}"; do - if ! command_exists "$package"; then - print_warning "$package not found" - install_apt "$package" - else - print_success "$package is installed" - fi - done - - # Check and install snap packages - for package in "${REQUIRED_SNAP_PACKAGES[@]}"; do - if ! snap list "$package" &> /dev/null; then - print_warning "$package not found" - install_snap "$package" - else - print_success "$package is installed" - fi - done -} - -# Main installation process -main() { - # Show help if requested - if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then - show_help - fi - - # Check for --all parameter - if [ "$1" = "--all" ]; then - check_and_install_tools - handle_config_directory - install_all_modules - exit 0 - fi - - # Check for --backup parameter - if [ "$1" = "--backup" ]; then - handle_config_directory - exit 0 - fi - - # Check for --update parameter - if [ "$1" = "--update" ]; then - update_all_packages - exit 0 - fi - - # Normal interactive mode - check_and_install_tools - handle_config_directory - show_menu -} - -# Run the main function -main "$@" + echo -e "${RED}[✗]${BASE} $1" +} + +case "$(uname -s)" in + Linux) + print_info "Detected Linux, using Ubuntu installer" + exec bash "$SCRIPT_DIR/install_ubuntu.sh" "$@" + ;; + Darwin) + print_info "Detected MacOS, using Homebrew installer" + exec bash "$SCRIPT_DIR/install_macos.sh" "$@" + ;; + *) + print_error "Unsupported operating system: $(uname -s)" + exit 1 + ;; +esac diff --git a/install_macos.sh b/install_macos.sh new file mode 100755 index 0000000..17fbd7b --- /dev/null +++ b/install_macos.sh @@ -0,0 +1,286 @@ +#!/bin/bash + +# Exit on any error +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKUP_DIR="$SCRIPT_DIR/backup" +BREWFILE="$SCRIPT_DIR/Brewfile" + +BASE="\033[0m" +TEXT="\033[38;2;205;214;244m" +SUBTEXT="\033[38;2;166;173;200m" +RED="\033[38;2;243;139;168m" +GREEN="\033[38;2;166;227;161m" +YELLOW="\033[38;2;249;226;175m" +BLUE="\033[38;2;137;180;250m" +MAUVE="\033[38;2;203;166;247m" + +declare -a MACOS_MODULE_ORDER=( + "zsh" + "hyfetch" + "vim" + "kitty" + "kubectl" + "github-cli" + "gitlab-cli" + "nvm" +) + +declare -a MACOS_SKIP_MODULES=( + "apt-packages" + "snap-config" + "flatpak-config" + "appimaged" + "certs" + "docker" + "slack" + "powershell" +) + +print_success() { + echo -e "${GREEN}[✓]${BASE} $1" +} + +print_error() { + echo -e "${RED}[✗]${BASE} $1" +} + +print_warning() { + echo -e "${YELLOW}[!]${BASE} $1" +} + +print_info() { + echo -e "${BLUE}[i]${BASE} $1" +} + +print_header() { + echo -e "\n${MAUVE}=== $1 ===${BASE}\n" +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +is_skipped_module() { + local module=$1 + local skipped + + for skipped in "${MACOS_SKIP_MODULES[@]}"; do + if [ "$module" = "$skipped" ]; then + return 0 + fi + done + + return 1 +} + +ensure_macos() { + if [ "$(uname -s)" != "Darwin" ]; then + print_error "This installer is for MacOS only. Use install_ubuntu.sh on Ubuntu." + exit 1 + fi +} + +ensure_homebrew() { + print_header "Checking Homebrew" + + if command_exists brew; then + print_success "Homebrew is installed" + return 0 + fi + + print_warning "Homebrew is not installed" + print_info "Installing Homebrew from the official installer..." + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + + if [ -x "/opt/homebrew/bin/brew" ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [ -x "/usr/local/bin/brew" ]; then + eval "$(/usr/local/bin/brew shellenv)" + fi + + if ! command_exists brew; then + print_error "Homebrew installation completed, but brew is not available in PATH" + print_warning "Open a new terminal or add Homebrew to your shell profile, then rerun this script" + exit 1 + fi + + print_success "Homebrew installed successfully" +} + +install_homebrew_bundle() { + print_header "Installing MacOS Packages" + + if [ ! -f "$BREWFILE" ]; then + print_error "Brewfile not found at $BREWFILE" + exit 1 + fi + + brew bundle --file="$BREWFILE" + print_success "Homebrew bundle completed" +} + +update_homebrew_packages() { + print_header "Updating Homebrew Packages" + ensure_homebrew + brew update + brew upgrade + brew upgrade --cask --greedy || true + brew cleanup + print_success "Homebrew packages updated" +} + +ensure_directories() { + mkdir -p "$BACKUP_DIR" "$HOME/.config" + print_success "Ensured backup and .config directories exist" +} + +install_module() { + local module=$1 + local module_dir="$SCRIPT_DIR/$module" + + if is_skipped_module "$module"; then + print_warning "Skipping MacOS-incompatible module: $module" + return 0 + fi + + if [ ! -f "$module_dir/install.sh" ]; then + print_warning "No install.sh found for module $module" + return 0 + fi + + print_info "Installing $module module..." + ( + cd "$module_dir" + bash ./install.sh + ) + print_success "$module installed successfully" +} + +install_all_modules() { + print_header "Installing All MacOS Modules" + + local module + for module in "${MACOS_MODULE_ORDER[@]}"; do + if [ -d "$SCRIPT_DIR/$module" ]; then + install_module "$module" + else + print_warning "Module $module not found, skipping..." + fi + done +} + +select_modules() { + print_header "Available MacOS Modules" + local i=1 + local modules=() + local dir module + + for dir in "$SCRIPT_DIR"/*/; do + module=$(basename "$dir") + if [ -f "${dir}install.sh" ] && ! is_skipped_module "$module"; then + echo -e "${TEXT}$i) ${BLUE}${module}${BASE}" + modules+=("$module") + ((i++)) + fi + done + + echo -e "\n${TEXT}$i) ${GREEN}Install selected${BASE}" + echo -e "${TEXT}$((i+1))) ${RED}Cancel${BASE}" + + local selected=() + local choice + while true; do + read -r -p "$(echo -e "${SUBTEXT}Select a module number (or $i to install, $((i+1)) to cancel): ${BASE}")" choice + + if [ "$choice" -eq "$i" ]; then + break + elif [ "$choice" -eq "$((i+1))" ]; then + print_info "Installation cancelled" + exit 0 + elif [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then + selected+=("${modules[$((choice-1))]}") + print_success "Module ${modules[$((choice-1))]} selected" + else + print_error "Invalid option" + fi + done + + print_header "Installing Selected MacOS Modules" + for module in "${selected[@]}"; do + install_module "$module" + done +} + +show_menu() { + print_header "MacOS Dotfiles Installation Menu" + echo -e "${TEXT}1) ${GREEN}Install all MacOS modules${BASE}" + echo -e "${TEXT}2) ${BLUE}Select MacOS modules to install${BASE}" + echo -e "${TEXT}3) ${RED}Exit${BASE}" + + read -r -p "$(echo -e "${SUBTEXT}Choose an option [1-3]: ${BASE}")" choice + + case $choice in + 1) install_all_modules ;; + 2) select_modules ;; + 3) + print_info "Exiting..." + exit 0 + ;; + *) + print_error "Invalid option. Exiting..." + exit 1 + ;; + esac +} + +show_help() { + print_header "MacOS Dotfiles Installation Help" + echo -e "${TEXT}Usage:${BASE}" + echo -e " ${BLUE}./install.sh${BASE} [options]" + echo + echo -e "${TEXT}Options:${BASE}" + echo -e " ${GREEN}--all${BASE} Install Homebrew packages and all MacOS modules" + echo -e " ${GREEN}--backup${BASE} Only ensure backup and .config directories exist" + echo -e " ${GREEN}--update${BASE} Update Homebrew packages" + echo -e " ${GREEN}--help${BASE} Show this help message" + echo + echo -e "${TEXT}MacOS modules:${BASE} ${BLUE}${MACOS_MODULE_ORDER[*]}${BASE}" + echo -e "${TEXT}Skipped Linux-only modules:${BASE} ${YELLOW}${MACOS_SKIP_MODULES[*]}${BASE}" + exit 0 +} + +main() { + ensure_macos + + case "${1:-}" in + --help|-h) + show_help + ;; + --all) + ensure_homebrew + install_homebrew_bundle + ensure_directories + install_all_modules + ;; + --backup) + ensure_directories + ;; + --update) + update_homebrew_packages + ;; + "") + ensure_homebrew + install_homebrew_bundle + ensure_directories + show_menu + ;; + *) + print_error "Unknown option: $1" + show_help + ;; + esac +} + +main "$@" diff --git a/install_ubuntu.sh b/install_ubuntu.sh new file mode 100755 index 0000000..514b8b5 --- /dev/null +++ b/install_ubuntu.sh @@ -0,0 +1,321 @@ +#!/bin/bash + +# Exit on any error +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKUP_DIR="$SCRIPT_DIR/backup" + +declare -a REQUIRED_APT_PACKAGES=( + "stow" + "git" + "wget" + "unzip" + "fontconfig" + "curl" + "libfuse2" +) + +declare -a REQUIRED_SNAP_PACKAGES=( + # Add snap packages here as needed +) + +BASE="\033[0m" +TEXT="\033[38;2;205;214;244m" +SUBTEXT="\033[38;2;166;173;200m" +RED="\033[38;2;243;139;168m" +GREEN="\033[38;2;166;227;161m" +YELLOW="\033[38;2;249;226;175m" +BLUE="\033[38;2;137;180;250m" +MAUVE="\033[38;2;203;166;247m" + +print_success() { + echo -e "${GREEN}[✓] $1${BASE}" +} + +print_error() { + echo -e "${RED}[✗] $1${BASE}" +} + +print_warning() { + echo -e "${YELLOW}[!] $1${BASE}" +} + +print_info() { + echo -e "${BLUE}[i] $1${BASE}" +} + +print_header() { + echo -e "\n${MAUVE}=== $1 ===${BASE}\n" +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +install_apt() { + local package=$1 + print_info "Installing $package via apt..." + sudo apt-get update + sudo apt-get install -y "$package" + print_success "$package installed successfully" +} + +install_snap() { + local package=$1 + print_info "Installing $package via snap..." + sudo snap install "$package" + print_success "$package installed successfully" +} + +update_apt_packages() { + print_header "Updating APT Packages" + sudo apt-get update + sudo apt-get upgrade -y + sudo apt-get full-upgrade -y + sudo apt-get autoremove -y + sudo apt-get autoclean + print_success "APT packages updated" +} + +update_snap_packages() { + print_header "Updating Snap Packages" + if command_exists snap; then + sudo snap refresh + print_success "Snap packages refreshed" + else + print_warning "snap is not installed, skipping Snap updates" + fi +} + +update_flatpak_packages() { + print_header "Updating Flatpak Packages" + if ! command_exists flatpak; then + print_warning "flatpak is not installed, skipping Flatpak updates" + return 0 + fi + + sudo flatpak update --system -y + print_success "Flatpak packages updated" +} + +update_all_packages() { + print_header "Updating All Ubuntu Packages" + update_apt_packages + update_snap_packages + update_flatpak_packages + print_success "All Ubuntu packages updated successfully!" +} + +show_help() { + print_header "Ubuntu Dotfiles Installation Help" + echo -e "${TEXT}Usage:${BASE}" + echo -e " ${BLUE}./install.sh${BASE} [options]" + echo + echo -e "${TEXT}Options:${BASE}" + echo -e " ${GREEN}--all${BASE} Install all Ubuntu modules in predefined order without prompts" + echo -e " ${GREEN}--backup${BASE} Only ensure the backup and .config directories exist" + echo -e " ${GREEN}--update${BASE} Update apt, snap, and flatpak packages" + echo -e " ${GREEN}--help${BASE} Show this help message" + echo + echo -e "${TEXT}Installation Order (--all):${BASE}" + echo -e " ${BLUE}1.${BASE} apt-packages" + echo -e " ${BLUE}2.${BASE} certs" + echo -e " ${BLUE}3.${BASE} zsh" + echo -e " ${BLUE}4.${BASE} hyfetch" + echo -e " ${BLUE}5.${BASE} snap-config" + echo -e " ${BLUE}6.${BASE} flatpak-config" + echo -e " ${BLUE}7.${BASE} vim" + echo -e " ${BLUE}8.${BASE} kitty" + echo -e " ${BLUE}9.${BASE} kubectl" + echo -e " ${BLUE}10.${BASE} github-cli" + echo -e " ${BLUE}11.${BASE} slack" + echo -e " ${BLUE}12.${BASE} docker" + echo -e " ${BLUE}13.${BASE} nvm" + echo -e " ${BLUE}14.${BASE} gitlab-cli" + exit 0 +} + +ensure_backup_dir() { + if [ ! -d "$BACKUP_DIR" ]; then + mkdir -p "$BACKUP_DIR" + print_success "Created backup directory at $BACKUP_DIR" + fi +} + +handle_config_directory() { + local config_path="$HOME/.config" + + if [ -d "$config_path" ]; then + print_warning "Found existing .config directory. No changes will be made to it by the main installer." + else + print_info "No existing .config directory found" + mkdir -p "$config_path" + print_success "Created new .config directory" + fi +} + +install_module() { + local module_dir=$1 + local module_name + module_name=$(basename "$module_dir") + + print_info "Installing $module_name configuration..." + + if [ -f "$SCRIPT_DIR/$module_dir/install.sh" ]; then + ( + cd "$SCRIPT_DIR/$module_dir" + bash ./install.sh + ) + print_success "$module_name installed successfully" + else + print_warning "No install.sh found in $module_name" + fi +} + +install_all_modules() { + print_header "Installing All Ubuntu Modules" + + declare -a MODULE_ORDER=( + "apt-packages" + "certs" + "zsh" + "hyfetch" + "snap-config" + "flatpak-config" + "vim" + "kitty" + "kubectl" + "github-cli" + "slack" + "docker" + "nvm" + "gitlab-cli" + ) + + for module in "${MODULE_ORDER[@]}"; do + if [ -d "$SCRIPT_DIR/$module" ]; then + install_module "$module" + else + print_warning "Module $module not found, skipping..." + fi + done +} + +select_modules() { + print_header "Available Ubuntu Modules" + local i=1 + local modules=() + + for dir in "$SCRIPT_DIR"/*/; do + local module_name + module_name=$(basename "$dir") + if [ "$module_name" != ".cursor" ] && [ "$module_name" != "backup" ] && [ -f "${dir}install.sh" ]; then + echo -e "${TEXT}$i) ${BLUE}${module_name}${BASE}" + modules+=("$module_name") + ((i++)) + fi + done + + echo -e "\n${TEXT}$i) ${GREEN}Install selected${BASE}" + echo -e "${TEXT}$((i+1))) ${RED}Cancel${BASE}" + + local selected=() + while true; do + read -r -p "$(echo -e "${SUBTEXT}Select a module number (or $i to install, $((i+1)) to cancel): ${BASE}")" choice + + if [ "$choice" -eq "$i" ]; then + break + elif [ "$choice" -eq "$((i+1))" ]; then + print_info "Installation cancelled" + exit 0 + elif [ "$choice" -ge 1 ] && [ "$choice" -lt "$i" ]; then + selected+=("${modules[$((choice-1))]}") + print_success "Module ${modules[$((choice-1))]} selected" + else + print_error "Invalid option" + fi + done + + print_header "Installing Selected Ubuntu Modules" + for module in "${selected[@]}"; do + install_module "$module" + done +} + +show_menu() { + print_header "Ubuntu Dotfiles Installation Menu" + echo -e "${TEXT}1) ${GREEN}Install all modules${BASE}" + echo -e "${TEXT}2) ${BLUE}Select modules to install${BASE}" + echo -e "${TEXT}3) ${RED}Exit${BASE}" + + read -r -p "$(echo -e "${SUBTEXT}Choose an option [1-3]: ${BASE}")" choice + + case $choice in + 1) install_all_modules ;; + 2) select_modules ;; + 3) + print_info "Exiting..." + exit 0 + ;; + *) + print_error "Invalid option. Exiting..." + exit 1 + ;; + esac +} + +check_and_install_tools() { + print_header "Checking Required Ubuntu Tools" + + for package in "${REQUIRED_APT_PACKAGES[@]}"; do + if ! command_exists "$package"; then + print_warning "$package not found" + install_apt "$package" + else + print_success "$package is installed" + fi + done + + for package in "${REQUIRED_SNAP_PACKAGES[@]}"; do + if ! snap list "$package" >/dev/null 2>&1; then + print_warning "$package not found" + install_snap "$package" + else + print_success "$package is installed" + fi + done +} + +main() { + case "${1:-}" in + --help|-h) + show_help + ;; + --all) + check_and_install_tools + ensure_backup_dir + handle_config_directory + install_all_modules + ;; + --backup) + ensure_backup_dir + handle_config_directory + ;; + --update) + update_all_packages + ;; + "") + check_and_install_tools + ensure_backup_dir + handle_config_directory + show_menu + ;; + *) + print_error "Unknown option: $1" + show_help + ;; + esac +} + +main "$@" diff --git a/kitty/install.sh b/kitty/install.sh index d55ba0f..1baa5d6 100755 --- a/kitty/install.sh +++ b/kitty/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="kitty" +OS_TYPE="$(uname -s)" # Catppuccin Mocha color scheme # Base colors @@ -67,7 +68,7 @@ if [ -d "$HOME/.config/kitty" ]; then TIMESTAMP=$(date +%Y%m%d_%H%M%S) tar -czf "$BACKUP_DIR/kitty_backup_$TIMESTAMP.tar.gz" -C "$HOME" .config/kitty print_success "Backup created at $BACKUP_DIR/kitty_backup_$TIMESTAMP.tar.gz" - + # Remove existing configuration after backup print_status "Removing existing Kitty configuration..." rm -rf "$HOME/.config/kitty" @@ -78,6 +79,13 @@ fi if [ -f /.dockerenv ]; then print_warning "Running in Docker environment - skipping Kitty installation" print_warning "Kitty requires a desktop environment and cannot be installed in Docker" +elif [ "$OS_TYPE" = "Darwin" ]; then + if command -v brew >/dev/null 2>&1 && ! brew list --cask kitty >/dev/null 2>&1; then + print_status "Installing Kitty via Homebrew..." + brew install --cask kitty + else + print_status "Kitty is already installed or Homebrew is unavailable" + fi else # Install Kitty if not already installed if ! command -v kitty &> /dev/null; then @@ -97,4 +105,4 @@ fi print_success "$MODULE_NAME configuration installed successfully!" if [ ! -f /.dockerenv ]; then print_warning "Please restart Kitty for the changes to take effect" -fi \ No newline at end of file +fi \ No newline at end of file diff --git a/kitty/test.sh b/kitty/test.sh index a283cef..8c24ef3 100755 --- a/kitty/test.sh +++ b/kitty/test.sh @@ -79,6 +79,14 @@ test_stow_link() { print_header "Testing $MODULE_NAME configuration" +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + # shellcheck source=../scripts/test-lib.sh + . "$SCRIPT_DIR/../scripts/test-lib.sh" + test_stow_link_portable "$HOME/.config/kitty" "$SCRIPT_DIR/.config/kitty" + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test Kitty installation if not in Docker if [ ! -f /.dockerenv ]; then if command -v kitty &> /dev/null; then diff --git a/kubectl/.kube/config b/kubectl/.kube/config index 2364aa2..f05ccd3 100644 --- a/kubectl/.kube/config +++ b/kubectl/.kube/config @@ -30,7 +30,7 @@ users: - --listen-address=127.0.0.1:8000 - --oidc-issuer-url=https://iam.steelhome.internal/auth/realms/steelhome - --oidc-client-id=mtMC-K8S-prd - command: bin/kubelogin + command: kubelogin env: null interactiveMode: IfAvailable provideClusterInfo: false diff --git a/kubectl/.stow-local-ignore b/kubectl/.stow-local-ignore index 6f78cae..fadaf16 100644 --- a/kubectl/.stow-local-ignore +++ b/kubectl/.stow-local-ignore @@ -1,3 +1,4 @@ install.sh -test.sh -README.md \ No newline at end of file +test.sh +README.md +.config \ No newline at end of file diff --git a/kubectl/install.sh b/kubectl/install.sh index 79aa8fb..3a87935 100755 --- a/kubectl/install.sh +++ b/kubectl/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="kubectl" +OS_TYPE="$(uname -s)" # Catppuccin Mocha color scheme # Base colors @@ -68,7 +69,7 @@ detect_arch() { x86_64) echo "amd64" ;; - aarch64) + aarch64|arm64) echo "arm64" ;; armv7l) @@ -84,27 +85,37 @@ detect_arch() { # Function to install kubectl install_kubectl() { local OS=$(detect_os) + local ARCH=$(detect_arch) print_status "Installing kubectl for $OS..." - + + if [ "$OS_TYPE" = "Darwin" ]; then + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install kubectl on MacOS" + exit 1 + fi + brew install kubectl + return 0 + fi + # Create temporary directory local TEMP_DIR=$(mktemp -d) cd "$TEMP_DIR" - + # Download latest stable kubectl print_status "Downloading latest stable kubectl..." - curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" - + curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/${ARCH}/kubectl" + # Make it executable chmod +x kubectl - + # Move to system directory print_status "Installing kubectl to system..." sudo mv kubectl /usr/local/bin/ - + # Cleanup cd - > /dev/null rm -rf "$TEMP_DIR" - + # Verify installation if command -v kubectl &> /dev/null; then print_success "kubectl installed successfully!" @@ -120,38 +131,45 @@ install_kubelogin() { local OS=$(detect_os) local ARCH=$(detect_arch) print_status "Installing kubelogin for $OS..." - + + if [ "$OS_TYPE" = "Darwin" ]; then + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install kubelogin on MacOS" + exit 1 + fi + brew install int128/kubelogin/kubelogin + return 0 + fi + # Create temporary directory local TEMP_DIR=$(mktemp -d) cd "$TEMP_DIR" - + # Get latest version local LATEST_VERSION=$(curl -s https://api.github.com/repos/int128/kubelogin/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - + # Download kubelogin print_status "Downloading kubelogin version $LATEST_VERSION..." curl -LO "https://github.com/int128/kubelogin/releases/download/${LATEST_VERSION}/kubelogin_linux_${ARCH}.zip" - + # Unzip and install print_status "Installing kubelogin..." unzip "kubelogin_linux_${ARCH}.zip" chmod +x kubelogin - - # Create krew bin directory for int128 - print_status "Creating krew bin directory for int128..." - mkdir -p "$HOME/.kube/bin/" - - # Move kubelogin to krew bin directory - mv kubelogin "$HOME/.kube/bin/" - + + # Install into the user's PATH so kubeconfig can use "command: kubelogin". + print_status "Installing kubelogin to user bin directory..." + mkdir -p "$HOME/.local/bin" + mv kubelogin "$HOME/.local/bin/kubelogin" + # Cleanup cd - > /dev/null rm -rf "$TEMP_DIR" - + # Verify installation - if [ -f "$HOME/.kube/bin/kubelogin" ]; then + if [ -f "$HOME/.local/bin/kubelogin" ]; then print_success "kubelogin installed successfully!" - "$HOME/.kube/bin/kubelogin" version + "$HOME/.local/bin/kubelogin" version else print_error "Failed to install kubelogin" exit 1 @@ -160,15 +178,30 @@ install_kubelogin() { # Function to backup existing config backup_config() { - if [ -f ~/.kube/config ]; then + local config_path="$HOME/.kube/config" + + if [ -e "$config_path" ] || [ -L "$config_path" ]; then local BACKUP_DIR="$SCRIPT_DIR/../backup/modules/$MODULE_NAME" local TIMESTAMP=$(date +%Y%m%d_%H%M%S) - local BACKUP_FILE="$BACKUP_DIR/$TIMESTAMP" - + local BACKUP_PATH="$BACKUP_DIR/$TIMESTAMP" + print_status "Backing up existing kubectl config..." - mkdir -p "$BACKUP_DIR" - cp ~/.kube/config "$BACKUP_FILE" - print_success "Backup created at $BACKUP_FILE" + mkdir -p "$BACKUP_PATH" + + if [ -L "$config_path" ]; then + local symlink_target + symlink_target="$(readlink "$config_path" || true)" + case "$symlink_target" in + "$SCRIPT_DIR"/*|../"$SCRIPT_DIR"/*) + print_status "Removing existing repository-managed kubectl config symlink" + rm "$config_path" + return 0 + ;; + esac + fi + + mv "$config_path" "$BACKUP_PATH/config" + print_success "Backup created at $BACKUP_PATH/config" fi } @@ -182,7 +215,7 @@ if ! command -v kubectl &> /dev/null; then fi # Check if kubelogin is installed -if [ ! -f "$HOME/.krew/bin/int128/kubelogin" ]; then +if ! command -v kubelogin >/dev/null 2>&1 && [ ! -f "$HOME/.local/bin/kubelogin" ]; then print_warning "kubelogin is not installed." install_kubelogin fi @@ -218,7 +251,7 @@ print_success "$MODULE_NAME configuration installed successfully!" # Display next steps print_header "Next Steps" print_warning "Please complete the following manually:" -print_warning "1. Add krew bin to your PATH if not already done:" -print_warning " echo 'export PATH=\"\$PATH:\$HOME/.krew/bin\"' >> ~/.zshrc" +print_warning "1. Ensure user binaries are in your PATH if not already done:" +print_warning " echo 'export PATH=\"\$PATH:\$HOME/.local/bin\"' >> ~/.zshrc" print_warning "2. Restart your shell or run 'source ~/.zshrc' to apply changes" -print_warning "3. Test kubelogin: $HOME/.krew/bin/int128/kubelogin version" \ No newline at end of file +print_warning "3. Test kubelogin: kubelogin version" \ No newline at end of file diff --git a/kubectl/test.sh b/kubectl/test.sh index b822053..30531ca 100755 --- a/kubectl/test.sh +++ b/kubectl/test.sh @@ -49,35 +49,15 @@ print_header() { echo -e "\n${MAUVE}=== $1 ===${BASE}\n" } +# shellcheck source=../scripts/test-lib.sh +. "$SCRIPT_DIR/../scripts/test-lib.sh" + # Test functions test_stow_link() { local target="$1" local source="$2" - - print_status "Testing stow link: $target" - - # Check if the target is a symbolic link - if [ ! -L "$target" ]; then - print_error "$target is not a symbolic link" - return 1 - fi - - # Get the absolute path of the source - local abs_source="$(cd "$(dirname "$source")" && pwd)/$(basename "$source")" - - # Get the absolute path of the target's link - local abs_target="$(readlink -f "$target")" - - # Compare the paths - if [ "$abs_target" = "$abs_source" ]; then - print_success "$target is properly linked by stow" - return 0 - else - print_error "$target is not properly linked by stow" - print_error "Expected: $abs_source" - print_error "Got: $abs_target" - return 1 - fi + + test_stow_link_portable "$target" "$source" } test_file_exists() { @@ -106,9 +86,36 @@ test_dependency() { fi } +test_stow_dir_or_nested_file() { + local target_dir="$1" + local source_dir="$2" + local nested_file="$3" + + if [ -L "$target_dir" ]; then + test_stow_link "$target_dir" "$source_dir" + else + test_stow_link "$target_dir/$nested_file" "$source_dir/$nested_file" + fi +} + # Main test execution print_header "Testing $MODULE_NAME configuration" +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + test_stow_dir_or_nested_file "$CONFIG_DIR" "$SCRIPT_DIR/.config/$MODULE_NAME" "config.zsh" + test_stow_dir_or_nested_file "$HOME/.kube" "$SCRIPT_DIR/.kube" "config" + + if grep -q 'command: kubelogin' "$HOME/.kube/config"; then + print_success "kubeconfig uses PATH-based kubelogin" + else + print_error "kubeconfig does not use PATH-based kubelogin" + exit 1 + fi + + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test dependencies test_dependency "kubectl" || exit 1 test_dependency "kubelogin" || exit 1 @@ -165,8 +172,8 @@ else fi # Test stow links -test_stow_link "$CONFIG_DIR" "$SCRIPT_DIR/.config/$MODULE_NAME" || exit 1 -test_stow_link "$HOME/.kube" "$SCRIPT_DIR/.kube" || exit 1 +test_stow_dir_or_nested_file "$CONFIG_DIR" "$SCRIPT_DIR/.config/$MODULE_NAME" "config.zsh" || exit 1 +test_stow_dir_or_nested_file "$HOME/.kube" "$SCRIPT_DIR/.kube" "config" || exit 1 # Test OIDC configuration print_status "Testing OIDC configuration" diff --git a/nvm/install.sh b/nvm/install.sh index 9960474..42ce708 100755 --- a/nvm/install.sh +++ b/nvm/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="nvm" +OS_TYPE="$(uname -s)" # NVM version to install (latest stable) NVM_VERSION="v0.40.3" @@ -65,7 +66,9 @@ if [ "$EUID" -eq 0 ]; then fi # Determine NVM directory -if [ -z "${XDG_CONFIG_HOME-}" ]; then +if [ "$OS_TYPE" = "Darwin" ]; then + NVM_DIR="$HOME/.nvm" +elif [ -z "${XDG_CONFIG_HOME-}" ]; then NVM_DIR="$HOME/.nvm" else NVM_DIR="$XDG_CONFIG_HOME/nvm" @@ -73,18 +76,38 @@ fi # Function to check if NVM is already installed check_nvm_installed() { + if [ -s "$NVM_DIR/nvm.sh" ] || nvm_source_file >/dev/null 2>&1; then + return 0 + fi + return 1 +} + +nvm_source_file() { if [ -s "$NVM_DIR/nvm.sh" ]; then + printf '%s\n' "$NVM_DIR/nvm.sh" return 0 fi + + if [ "$OS_TYPE" = "Darwin" ] && command -v brew >/dev/null 2>&1; then + local brew_nvm_prefix + brew_nvm_prefix="$(brew --prefix nvm 2>/dev/null || true)" + if [ -n "$brew_nvm_prefix" ] && [ -s "$brew_nvm_prefix/nvm.sh" ]; then + printf '%s\n' "$brew_nvm_prefix/nvm.sh" + return 0 + fi + fi + return 1 } # Function to check if Node.js is installed via NVM check_node_installed() { # Source NVM if available - if [ -s "$NVM_DIR/nvm.sh" ]; then + local nvm_file + nvm_file="$(nvm_source_file || true)" + if [ -n "$nvm_file" ]; then # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" + . "$nvm_file" if command -v node >/dev/null 2>&1 && [ -n "$(nvm current 2>/dev/null)" ] && [ "$(nvm current)" != "none" ]; then return 0 fi @@ -95,7 +118,18 @@ check_node_installed() { # Function to install NVM install_nvm() { print_status "Installing NVM..." - + + if [ "$OS_TYPE" = "Darwin" ]; then + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install NVM on MacOS" + exit 1 + fi + brew install nvm + mkdir -p "$NVM_DIR" + print_success "NVM installed successfully" + return 0 + fi + # Check if curl or wget is available if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then print_error "curl or wget is required to install NVM" @@ -103,7 +137,7 @@ install_nvm() { sudo apt-get update sudo apt-get install -y curl fi - + # Install NVM using the official install script if command -v curl >/dev/null 2>&1; then print_status "Downloading and installing NVM using curl..." @@ -115,7 +149,7 @@ install_nvm() { print_error "Neither curl nor wget is available" exit 1 fi - + # Verify installation if [ -s "$NVM_DIR/nvm.sh" ]; then print_success "NVM installed successfully" @@ -128,7 +162,7 @@ install_nvm() { # Function to source NVM in shell configuration setup_shell_config() { print_status "Setting up shell configuration for NVM..." - + # Determine which shell config file to use if [ -n "$ZSH_VERSION" ]; then SHELL_CONFIG="$HOME/.zshrc" @@ -141,18 +175,28 @@ setup_shell_config() { else SHELL_CONFIG="$HOME/.profile" fi - + # NVM configuration snippet - NVM_SNIPPET="export NVM_DIR=\"\$([ -z \"\${XDG_CONFIG_HOME-}\" ] && printf %s \"\${HOME}/.nvm\" || printf %s \"\${XDG_CONFIG_HOME}/nvm\")\" + if [ "$OS_TYPE" = "Darwin" ] && command -v brew >/dev/null 2>&1 && brew --prefix nvm >/dev/null 2>&1; then + BREW_NVM_PREFIX="$(brew --prefix nvm)" + NVM_SNIPPET="export NVM_DIR=\"\$HOME/.nvm\" +[ -s \"$BREW_NVM_PREFIX/nvm.sh\" ] && \. \"$BREW_NVM_PREFIX/nvm.sh\" # This loads nvm +[ -s \"$BREW_NVM_PREFIX/etc/bash_completion.d/nvm\" ] && \. \"$BREW_NVM_PREFIX/etc/bash_completion.d/nvm\" # This loads nvm bash_completion" + else + NVM_SNIPPET="export NVM_DIR=\"\$([ -z \"\${XDG_CONFIG_HOME-}\" ] && printf %s \"\${HOME}/.nvm\" || printf %s \"\${XDG_CONFIG_HOME}/nvm\")\" [ -s \"\$NVM_DIR/nvm.sh\" ] && \. \"\$NVM_DIR/nvm.sh\" # This loads nvm [ -s \"\$NVM_DIR/bash_completion\" ] && \. \"\$NVM_DIR/bash_completion\" # This loads nvm bash_completion" - + fi + # Check if NVM is already configured if [ -f "$SHELL_CONFIG" ] && grep -q "NVM_DIR" "$SHELL_CONFIG" && grep -q "nvm.sh" "$SHELL_CONFIG"; then print_status "NVM is already configured in $SHELL_CONFIG" + elif [ -L "$SHELL_CONFIG" ]; then + print_warning "$SHELL_CONFIG is a symlink managed by stow; leaving it unchanged" + print_warning "Ensure the managed shell configuration loads NVM before opening a new shell" else print_status "Adding NVM configuration to $SHELL_CONFIG..." - + # Backup existing shell config if [ -f "$SHELL_CONFIG" ]; then TIMESTAMP=$(date +%Y-%m-%d_%H-%M-%S) @@ -161,7 +205,7 @@ setup_shell_config() { cp "$SHELL_CONFIG" "$BACKUP_PATH/$(basename "$SHELL_CONFIG")" print_success "Backup created at $BACKUP_PATH/$(basename "$SHELL_CONFIG")" fi - + # Append NVM configuration echo "" >> "$SHELL_CONFIG" echo "# NVM configuration" >> "$SHELL_CONFIG" @@ -173,26 +217,28 @@ setup_shell_config() { # Function to install Node.js install_node() { print_status "Installing Node.js using NVM..." - + # Source NVM - if [ -s "$NVM_DIR/nvm.sh" ]; then + local nvm_file + nvm_file="$(nvm_source_file || true)" + if [ -n "$nvm_file" ]; then # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" + . "$nvm_file" else print_error "NVM is not available" exit 1 fi - + # Install Node.js LTS print_status "Installing Node.js $NODE_VERSION..." if nvm install "$NODE_VERSION"; then print_success "Node.js installed successfully" - + # Set as default print_status "Setting Node.js as default..." nvm alias default "$NODE_VERSION" nvm use default - + # Display versions local node_version local npm_version @@ -209,12 +255,12 @@ install_node() { # Function to backup and install .npmrc configuration install_npmrc() { print_status "Installing .npmrc configuration..." - + # Ensure backup directory exists if [ ! -d "$BACKUP_DIR/modules/$MODULE_NAME" ]; then mkdir -p "$BACKUP_DIR/modules/$MODULE_NAME" fi - + # Backup existing .npmrc if it exists if [ -f "$HOME/.npmrc" ]; then if [ -L "$HOME/.npmrc" ]; then @@ -227,22 +273,22 @@ install_npmrc() { mkdir -p "$BACKUP_PATH" cp "$HOME/.npmrc" "$BACKUP_PATH/.npmrc" print_success "Backup created at $BACKUP_PATH/.npmrc" - + # Remove existing file to allow stow to create symlink rm -f "$HOME/.npmrc" fi fi - + # Change to script directory for stow cd "$SCRIPT_DIR" || exit 1 - + # Use stow to create symlink for .npmrc # This will create $HOME/.npmrc -> nvm/.npmrc if ! stow -t "$HOME" .; then print_error "Failed to install .npmrc configuration" exit 1 fi - + print_success ".npmrc configuration installed successfully" print_warning "Please update the following placeholders in $HOME/.npmrc:" print_warning " - Replace 'your.email@itsf.io' with your actual ITSF email" @@ -269,9 +315,10 @@ fi setup_shell_config # Source NVM for current session -if [ -s "$NVM_DIR/nvm.sh" ]; then +nvm_file="$(nvm_source_file || true)" +if [ -n "$nvm_file" ]; then # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" + . "$nvm_file" else print_error "Failed to source NVM" exit 1 @@ -280,10 +327,9 @@ fi # Check if Node.js is already installed if check_node_installed; then print_status "Node.js is already installed via NVM" - local current_version current_version=$(nvm current 2>/dev/null || echo "unknown") print_success "Current Node.js version: $current_version" - + # Check if we should update to latest LTS print_status "Checking for latest LTS version..." nvm install "$NODE_VERSION" --reinstall-packages-from=default diff --git a/nvm/test.sh b/nvm/test.sh index fe80f35..4f30052 100755 --- a/nvm/test.sh +++ b/nvm/test.sh @@ -354,6 +354,22 @@ test_stow_link() { # Main test execution print_header "Testing $MODULE_NAME configuration" +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + # shellcheck source=../scripts/test-lib.sh + . "$SCRIPT_DIR/../scripts/test-lib.sh" + test_stow_link_portable "$HOME/.npmrc" "$SCRIPT_DIR/.npmrc" + + if grep -q "registry=https://jfrog-artifactory.steelhome.internal" "$HOME/.npmrc" 2>/dev/null; then + print_success ".npmrc contains JFrog Artifactory registry configuration" + else + print_error ".npmrc does not contain JFrog Artifactory registry configuration" + exit 1 + fi + + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test NVM installation test_nvm_directory test_nvm_script diff --git a/scripts/test-lib.sh b/scripts/test-lib.sh new file mode 100644 index 0000000..5a8a863 --- /dev/null +++ b/scripts/test-lib.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Shared helpers for portable module tests. + +resolve_path_portable() { + local path="$1" + local target + + if [ -L "$path" ]; then + target="$(readlink "$path")" + case "$target" in + /*) path="$target" ;; + *) path="$(dirname "$path")/$target" ;; + esac + fi + + ( + cd "$(dirname "$path")" + printf "%s/%s\n" "$(pwd -P)" "$(basename "$path")" + ) +} + +test_stow_link_portable() { + local target="$1" + local source="$2" + + print_status "Testing stow link: $target" + + if [ ! -L "$target" ]; then + print_error "$target is not a symbolic link" + return 1 + fi + + local abs_source + local abs_target + abs_source="$(resolve_path_portable "$source")" + abs_target="$(resolve_path_portable "$target")" + + if [ "$abs_target" = "$abs_source" ]; then + print_success "$target is properly linked by stow" + return 0 + fi + + print_error "$target is not properly linked by stow" + print_error "Expected: $abs_source" + print_error "Got: $abs_target" + return 1 +} diff --git a/test-macos.sh b/test-macos.sh new file mode 100755 index 0000000..f63909b --- /dev/null +++ b/test-macos.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +# Exit on error +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BREWFILE="$SCRIPT_DIR/Brewfile" + +BASE="\033[0m" +RED="\033[38;2;243;139;168m" +GREEN="\033[38;2;166;227;161m" +YELLOW="\033[38;2;249;226;175m" +BLUE="\033[38;2;137;180;250m" +MAUVE="\033[38;2;203;166;247m" + +print_status() { + echo -e "${BLUE}[i]${BASE} $1" +} + +print_success() { + echo -e "${GREEN}[✓]${BASE} $1" +} + +print_error() { + echo -e "${RED}[✗]${BASE} $1" +} + +print_warning() { + echo -e "${YELLOW}[!]${BASE} $1" +} + +print_header() { + echo -e "\n${MAUVE}=== $1 ===${BASE}\n" +} + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +test_file_exists() { + local file="$1" + + if [ -f "$file" ]; then + print_success "File exists: $file" + else + print_error "Missing file: $file" + return 1 + fi +} + +test_bash_syntax() { + local file="$1" + print_status "Checking bash syntax: $file" + bash -n "$file" + print_success "Bash syntax is valid: $file" +} + +test_module_layout() { + local module="$1" + local module_dir="$SCRIPT_DIR/$module" + + test_file_exists "$module_dir/install.sh" + + case "$module" in + zsh|nvm) + test_file_exists "$module_dir/.stow-local-ignore" + ;; + *) + if [ -d "$module_dir/.config" ]; then + print_success "$module has a .config layout" + else + print_warning "$module does not use .config directly" + fi + ;; + esac +} + +stow_for_test() { + local test_home="$1" + local module="$2" + local target="$3" + local package="${4:-.}" + + print_status "Stowing $module into test home" + ( + cd "$SCRIPT_DIR/$module" + stow -t "$target" "$package" + ) +} + +run_portable_module_test() { + local test_home="$1" + local module="$2" + + print_status "Running portable module test: $module" + ( + export HOME="$test_home" + export DOTFILES_TEST_MODE=1 + cd "$SCRIPT_DIR/$module" + bash ./test.sh + ) +} + +print_header "Testing MacOS dotfiles support" + +test_file_exists "$SCRIPT_DIR/install.sh" +test_file_exists "$SCRIPT_DIR/install_macos.sh" +test_file_exists "$SCRIPT_DIR/install_ubuntu.sh" +test_file_exists "$BREWFILE" + +test_bash_syntax "$SCRIPT_DIR/install.sh" +test_bash_syntax "$SCRIPT_DIR/install_macos.sh" +test_bash_syntax "$SCRIPT_DIR/install_ubuntu.sh" + +for module in zsh hyfetch vim kitty kubectl github-cli gitlab-cli nvm; do + test_module_layout "$module" + test_bash_syntax "$SCRIPT_DIR/$module/install.sh" +done + +if command_exists ruby; then + print_status "Checking Brewfile Ruby syntax..." + ruby -c "$BREWFILE" >/dev/null + print_success "Brewfile syntax is valid" +else + print_warning "Ruby is not available; skipping Brewfile syntax check" +fi + +if [ "$(uname -s)" = "Darwin" ] && command_exists brew; then + print_status "Checking Homebrew bundle state..." + if brew bundle check --file="$BREWFILE"; then + print_success "Homebrew bundle is already satisfied" + else + print_warning "Homebrew bundle is valid but not fully installed yet" + print_warning "Run './install.sh --all' on MacOS to install missing packages" + fi +else + print_warning "Not running on MacOS with Homebrew; skipping brew bundle check" +fi + +print_header "Running portable module tests" +TEST_HOME="$(mktemp -d)" +mkdir -p "$TEST_HOME/.config" + +stow_for_test "$TEST_HOME" zsh "$TEST_HOME" +stow_for_test "$TEST_HOME" nvm "$TEST_HOME" +stow_for_test "$TEST_HOME" hyfetch "$TEST_HOME/.config" ".config" +stow_for_test "$TEST_HOME" vim "$TEST_HOME/.config" ".config" +stow_for_test "$TEST_HOME" kitty "$TEST_HOME/.config" ".config" +stow_for_test "$TEST_HOME" kubectl "$TEST_HOME/.config" ".config" +stow_for_test "$TEST_HOME" kubectl "$TEST_HOME" +stow_for_test "$TEST_HOME" github-cli "$TEST_HOME/.config" ".config" +stow_for_test "$TEST_HOME" gitlab-cli "$TEST_HOME/.config" ".config" + +for module in zsh nvm hyfetch vim kitty kubectl github-cli gitlab-cli; do + run_portable_module_test "$TEST_HOME" "$module" +done + +print_success "MacOS validation completed" diff --git a/vim/install.sh b/vim/install.sh index fd239f5..fbf9f3f 100755 --- a/vim/install.sh +++ b/vim/install.sh @@ -5,6 +5,7 @@ set -e # Script directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +OS_TYPE="$(uname -s)" # Dependencies declare -a REQUIRED_PACKAGES=( @@ -36,19 +37,56 @@ command_exists() { install_package() { local package=$1 print_info "Installing $package..." - sudo apt-get update - sudo apt-get install -y "$package" + if [ "$OS_TYPE" = "Darwin" ]; then + if ! command_exists brew; then + print_error "Homebrew is required to install $package on MacOS" + exit 1 + fi + brew install "$package" + else + sudo apt-get update + sudo apt-get install -y "$package" + fi print_success "$package installed successfully" } +package_command() { + case "$1" in + neovim) echo "nvim" ;; + fd-find) + if [ "$OS_TYPE" = "Darwin" ]; then + echo "fd" + else + echo "fdfind" + fi + ;; + *) echo "$1" ;; + esac +} + +package_name() { + case "$1" in + fd-find) + if [ "$OS_TYPE" = "Darwin" ]; then + echo "fd" + else + echo "fd-find" + fi + ;; + *) echo "$1" ;; + esac +} + # Check and install dependencies print_info "Checking dependencies..." for package in "${REQUIRED_PACKAGES[@]}"; do - if ! command_exists "$package"; then - print_info "$package not found. Installing..." - install_package "$package" + command_name="$(package_command "$package")" + install_name="$(package_name "$package")" + if ! command_exists "$command_name"; then + print_info "$command_name not found. Installing $install_name..." + install_package "$install_name" else - print_success "$package is already installed" + print_success "$command_name is already installed" fi done @@ -65,4 +103,4 @@ print_success "Neovim configuration installed successfully!" print_info "First time setup:" print_info "1. Open Neovim with 'nvim'" print_info "2. Wait for plugins to install" -print_info "3. Restart Neovim" \ No newline at end of file +print_info "3. Restart Neovim" \ No newline at end of file diff --git a/vim/test.sh b/vim/test.sh index b546e95..275ff41 100755 --- a/vim/test.sh +++ b/vim/test.sh @@ -26,6 +26,25 @@ command_exists() { # Check if Neovim is installed print_info "Checking if Neovim is installed..." + +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + CONFIG_FILES=( + "$HOME/.config/nvim/init.lua" + "$HOME/.config/nvim/lua/theme.lua" + ) + + for file in "${CONFIG_FILES[@]}"; do + if [ ! -f "$file" ]; then + print_error "Configuration file not found: $file" + exit 1 + fi + print_success "Found $file" + done + + print_success "Portable vim tests completed" + exit 0 +fi + if ! command_exists nvim; then print_error "Neovim is not installed" exit 1 diff --git a/zsh/.zshrc b/zsh/.zshrc index 62bcf9e..d1f1af6 100644 --- a/zsh/.zshrc +++ b/zsh/.zshrc @@ -21,13 +21,23 @@ else fi # Path configuration -export PATH=$PATH:$HOME/.local/bin +path=("$HOME/.local/bin" "$HOME/go/bin" $path) # Editor configuration -export EDITOR="/usr/bin/vim" +if command -v nvim >/dev/null 2>&1; then + export EDITOR="$(command -v nvim)" +elif command -v vim >/dev/null 2>&1; then + export EDITOR="$(command -v vim)" +fi # FZF configuration [ -f ~/.fzf.zsh ] && source ~/.fzf.zsh +if command -v brew >/dev/null 2>&1 && [ -d "$(brew --prefix fzf 2>/dev/null)/shell" ]; then + FZF_HOMEBREW_SHELL="$(brew --prefix fzf)/shell" + [ -f "$FZF_HOMEBREW_SHELL/completion.zsh" ] && source "$FZF_HOMEBREW_SHELL/completion.zsh" + [ -f "$FZF_HOMEBREW_SHELL/key-bindings.zsh" ] && source "$FZF_HOMEBREW_SHELL/key-bindings.zsh" + unset FZF_HOMEBREW_SHELL +fi export FZF_DEFAULT_OPTS="--height 40% --layout=reverse --border --preview 'bat --color=always --style=numbers --line-range=:500 {}'" export FZF_CTRL_T_OPTS="--preview 'bat --color=always --style=numbers --line-range=:500 {}'" @@ -79,7 +89,11 @@ alias ...='cd ../..' alias mkdir='mkdir -p' alias df='df -h' alias du='du -h' -alias free='free -h' +if command -v free >/dev/null 2>&1; then + alias free='free -h' +elif command -v vm_stat >/dev/null 2>&1; then + alias free='vm_stat' +fi alias grep='grep --color=auto' alias egrep='egrep --color=auto' alias fgrep='fgrep --color=auto' @@ -105,19 +119,24 @@ alias fzfp='fzf --preview "bat --color=always --style=numbers --line-range=:500 #THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!! export SDKMAN_DIR="$HOME/.sdkman" [[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh" -export PATH="$PATH:$HOME/.fzf/bin" +[ -d "$HOME/.fzf/bin" ] && path=("$HOME/.fzf/bin" $path) export NVM_DIR="$HOME/.nvm" -[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm -[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion - +if [ -s "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" + [ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion" +elif command -v brew >/dev/null 2>&1 && [ -s "$(brew --prefix nvm 2>/dev/null)/nvm.sh" ]; then + NVM_HOMEBREW_PREFIX="$(brew --prefix nvm)" + . "$NVM_HOMEBREW_PREFIX/nvm.sh" + [ -s "$NVM_HOMEBREW_PREFIX/etc/bash_completion.d/nvm" ] && . "$NVM_HOMEBREW_PREFIX/etc/bash_completion.d/nvm" + unset NVM_HOMEBREW_PREFIX +fi # Kitty and SSH # https://wiki.archlinux.org/title/Kitty#Terminal_issues_with_SSH [ "$TERM" = "xterm-kitty" ] && alias ssh="kitty +kitten ssh" # go -export PATH=$PATH:/usr/local/go/bin - -# go bin -export PATH=$PATH:~/go/bin +for go_path in /usr/local/go/bin /opt/homebrew/opt/go/bin; do + [ -d "$go_path" ] && path=("$go_path" $path) +done diff --git a/zsh/install.sh b/zsh/install.sh index bda9ab8..7245c5c 100755 --- a/zsh/install.sh +++ b/zsh/install.sh @@ -7,6 +7,7 @@ set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" BACKUP_DIR="$SCRIPT_DIR/../backup" MODULE_NAME="zsh" +OS_TYPE="$(uname -s)" # Catppuccin Mocha color scheme # Base colors @@ -52,11 +53,34 @@ print_header() { # Function to check if a font is installed check_font_installed() { local font_name="$1" - if fc-list | grep -i "$font_name" &> /dev/null; then - return 0 - else - return 1 + case "$OS_TYPE" in + Darwin) + system_profiler SPFontsDataType 2>/dev/null | grep -i "$font_name" &> /dev/null + ;; + *) + fc-list | grep -i "$font_name" &> /dev/null + ;; + esac +} + +install_brew_package() { + local package="$1" + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install $package on MacOS" + exit 1 + fi + + brew install "$package" +} + +install_brew_cask() { + local cask="$1" + if ! command -v brew >/dev/null 2>&1; then + print_error "Homebrew is required to install $cask on MacOS" + exit 1 fi + + brew install --cask "$cask" } # Print module header @@ -74,8 +98,12 @@ mkdir -p "$BACKUP_DIR" # Install ZSH if not already installed if ! command -v zsh &> /dev/null; then print_status "Installing ZSH..." - sudo apt-get update - sudo apt-get install -y zsh + if [ "$OS_TYPE" = "Darwin" ]; then + install_brew_package "zsh" + else + sudo apt-get update + sudo apt-get install -y zsh + fi fi # Remove Oh My Posh if installed @@ -107,13 +135,17 @@ if check_font_installed "Hack Nerd Font"; then print_status "Hack Nerd Font is already installed" else print_status "Installing Hack Nerd Font..." - FONT_DIR="$HOME/.local/share/fonts" - mkdir -p "$FONT_DIR" - wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.1.1/Hack.zip -O /tmp/hack.zip - unzip -q /tmp/hack.zip -d /tmp/hack - cp /tmp/hack/*.ttf "$FONT_DIR" - rm -rf /tmp/hack /tmp/hack.zip - fc-cache -f -v + if [ "$OS_TYPE" = "Darwin" ]; then + install_brew_cask "font-hack-nerd-font" + else + FONT_DIR="$HOME/.local/share/fonts" + mkdir -p "$FONT_DIR" + wget https://github.com/ryanoasis/nerd-fonts/releases/download/v3.1.1/Hack.zip -O /tmp/hack.zip + unzip -q /tmp/hack.zip -d /tmp/hack + cp /tmp/hack/*.ttf "$FONT_DIR" + rm -rf /tmp/hack /tmp/hack.zip + fc-cache -f -v + fi fi # Install ZSH plugins @@ -158,18 +190,30 @@ if command -v fzf >/dev/null 2>&1; then print_status "fzf is already installed. Skipping installation." else print_status "Installing fzf..." - if [ -d "$HOME/.fzf" ]; then - print_warning "fzf directory already exists, removing it..." - rm -rf "$HOME/.fzf" + if [ "$OS_TYPE" = "Darwin" ]; then + install_brew_package "fzf" + else + if [ -d "$HOME/.fzf" ]; then + print_warning "fzf directory already exists, removing it..." + rm -rf "$HOME/.fzf" + fi + git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf + ~/.fzf/install --all fi - git clone --depth 1 https://github.com/junegunn/fzf.git ~/.fzf - ~/.fzf/install --all fi # Set ZSH as default shell if not already if [ "$SHELL" != "$(which zsh)" ]; then print_status "Setting ZSH as default shell..." - sudo chsh -s "$(which zsh)" + if [ "$OS_TYPE" = "Darwin" ]; then + if ! grep -q "^$(which zsh)$" /etc/shells; then + print_status "Adding $(which zsh) to /etc/shells..." + echo "$(which zsh)" | sudo tee -a /etc/shells >/dev/null + fi + chsh -s "$(which zsh)" + else + sudo chsh -s "$(which zsh)" + fi print_warning "Please log out and log back in for the changes to take effect" fi @@ -195,4 +239,4 @@ fi print_success "$MODULE_NAME configuration installed successfully!" print_warning "Please set your terminal emulator to use 'Hack Nerd Font' for the best experience" -print_warning "You may need to restart your terminal for the font changes to take effect" \ No newline at end of file +print_warning "You may need to restart your terminal for the font changes to take effect" \ No newline at end of file diff --git a/zsh/test.sh b/zsh/test.sh index 3324f84..9160b68 100755 --- a/zsh/test.sh +++ b/zsh/test.sh @@ -109,6 +109,24 @@ test_oh_my_zsh_config() { print_header "Testing $MODULE_NAME configuration" TEST_FAILURES=0 +if [ "${DOTFILES_TEST_MODE:-0}" = "1" ]; then + # shellcheck source=../scripts/test-lib.sh + . "$SCRIPT_DIR/../scripts/test-lib.sh" + test_stow_link_portable "$HOME/.zshrc" "$SCRIPT_DIR/.zshrc" || TEST_FAILURES=$((TEST_FAILURES + 1)) + grep -q 'brew --prefix nvm' "$HOME/.zshrc" || { + print_error ".zshrc does not include the Homebrew NVM fallback" + TEST_FAILURES=$((TEST_FAILURES + 1)) + } + + if [ "$TEST_FAILURES" -gt 0 ]; then + print_error "$TEST_FAILURES test(s) failed" + exit 1 + fi + + print_success "Portable $MODULE_NAME tests completed" + exit 0 +fi + # Test basic installations if command -v zsh &> /dev/null; then print_success "ZSH is installed"