From 57403da32da78683b7509f66058d7d334dfb5e58 Mon Sep 17 00:00:00 2001 From: srdusr Date: Tue, 23 Sep 2025 20:57:55 +0200 Subject: Final updates, mostly everything kind of works --- README.md | 294 ++++++++++++++++++++++++++++++++++++++++------------ common/install.sh | 263 ++++++++++++++++++++++++++++++++++++++++++---- common/packages.yml | 5 +- 3 files changed, 469 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 33bfbdf..34d96d5 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,6 @@ Welcome, and make yourself at $HOME - Easy dotfiles management that respects the file hierarchy/XDG structure of the platform. - Custom `config` command that intelligently manages files across different operating systems. -Example: -```bash -config add .bashrc # → linux/home/.bashrc -config add /etc/issue # → linux/etc/issue -config commit -m "Updated dotfiles" -config push -u origin main -``` - --- ## Details @@ -50,7 +42,77 @@ Linux: --- -### Installing onto a new system (Manual) +## Usage Examples + +### Adding Files to Your Dotfiles + +```bash +# Add a config file explicitly to the common directory in the repo +config add --target common .bashrc + +# Add with a specific target directory +config add --target windows/Documents/PowerShell ~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 + +# Windows: +config add --target windows/Documents/PowerShell "$env:USERPROFILE\Documents\PowerShell\Microsoft.PowerShell_profile.ps1" + +# Linux WSL or Git Bash: +config add --target windows/Documents/PowerShell /mnt/c/Users/\/Documents/PowerShell/Microsoft.PowerShell_profile.ps1 + +# Add multiple files at once (each will be mapped appropriately) +config add ~/.vim .tmux.conf # Will go to OS's home + +# Add files outside of home +config add --target linux/etc /etc/issue + +``` + +## Installation Methods + +### Method 1: Shell Scripts (Recommended) + +**Linux/macOS:** +```sh +sh -c "$(curl -fsSL https://raw.githubusercontent.com/srdusr/dotfiles/main/common/install.sh)" +``` + +**Windows PowerShell:** +```powershell +iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/srdusr/dotfiles/main/windows/Documents/PowerShell/bootstrap.ps1')) + +# or + +$bat = "$env:TEMP\install.bat" +Invoke-WebRequest -Uri "https://raw.githubusercontent.com/srdusr/dotfiles/main/windows/install.bat" -OutFile $bat +cmd /c $bat +``` + +### Method 2: Ansible Automation + +Alternative to the shell scripts for managing multiple machines: + +```bash +# Clone repository +git clone https://github.com/srdusr/dotfiles.git +cd dotfiles/ansible + +# Install Ansible +pip install ansible + +# Deploy to localhost (replaces install.sh/bootstrap.ps1) +ansible-playbook -i inventory.yml playbook.yml -e dotfiles_profile=dev + +# Deploy to remote hosts +ansible-playbook -i inventory.yml playbook.yml --limit linux +``` + +**Note:** Both installation methods include: +- System hardening and security configurations +- Kernel/OS/distribution update checking +- Profile-based package installation +- Development environment setup + +### Method 3: Installing onto a new system (Manual) 1. Avoid weird behaviour/recursion issues when `.cfg` tries to track itself @@ -83,7 +145,6 @@ Copy and paste the following snippet to any profile/startup file ie. `~/.bashrc` ```bash # Dotfiles Management System if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then - # Core git wrapper with repository as work-tree _config() { git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" "$@" @@ -110,11 +171,9 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then # Check for paths that should go to the repository root case "$f" in common/*|linux/*|macos/*|windows/*|profile/*|README.md) - # If path already looks like a repo path, use it as is echo "$f" return ;; - # Otherwise, convert to a relative path "$HOME/"*) f="${f#$HOME/}" ;; @@ -124,7 +183,6 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then echo "$CFG_OS/home/$f" } - # Map repository path back to system path _sys_path() { local repo_path="$1" local os_path_pattern="$CFG_OS/" @@ -136,17 +194,50 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then fi case "$repo_path" in - # Files in the home directory + # Common configs → OS-specific config dirs + common/config/*) + case "$CFG_OS" in + linux) + local base="${XDG_CONFIG_HOME:-$HOME/.config}" + echo "$base/${repo_path#common/config/}" + ;; + macos) + echo "$HOME/Library/Application Support/${repo_path#common/config/}" + ;; + windows) + echo "$LOCALAPPDATA\\${repo_path#common/config/}" + ;; + *) + echo "$HOME/.config/${repo_path#common/config/}" + ;; + esac + ;; + + # Common assets → stay in repo + common/assets/*) + echo "$HOME/.cfg/$repo_path" + ;; + + # Other common files (dotfiles like .bashrc, .gitconfig, etc.) → $HOME + common/*) + echo "$HOME/${repo_path#common/}" + ;; + + # OS-specific home */home/*) echo "$HOME/${repo_path#*/home/}" ;; - # Other files in the repo root - common/*|profile/*|README.md|linux/*|macos/*|windows/*) + + # Profile configs and README → stay in repo + profile/*|README.md) echo "$HOME/.cfg/$repo_path" ;; + + # Default fallback *) - echo "/$repo_path" - ;; + echo "$HOME/.cfg/$repo_path" + ;; + esac } @@ -162,24 +253,55 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then elif command -v pkexec >/dev/null; then pkexec "$@" else - echo "Error: No privilege escalation tool (sudo, doas, pkexec) found." + echo "Error: No privilege escalation tool found." return 1 fi fi } - # NOTE: can change `config` to whatever you feel comfortable ie. dotfiles, dots, cfg etc. + # Main config command config() { local cmd="$1"; shift + local target_dir="" + # Parse optional --target flag for add + if [[ "$cmd" == "add" ]]; then + while [[ "$1" == --* ]]; do + case "$1" in + --target|-t) + target_dir="$2" + shift 2 + ;; + *) + echo "Unknown option: $1" + return 1 + ;; + esac + done + fi + case "$cmd" in add) local file_path for file_path in "$@"; do - local repo_path="$(_repo_path "$file_path")" + local repo_path + if [[ -n "$target_dir" ]]; then + local rel_path + if [[ "$file_path" == /* ]]; then + rel_path="$(basename "$file_path")" + else + rel_path="$file_path" + fi + repo_path="$target_dir/$rel_path" + else + repo_path="$(_repo_path "$file_path")" + fi + local full_repo_path="$HOME/.cfg/$repo_path" mkdir -p "$(dirname "$full_repo_path")" cp -a "$file_path" "$full_repo_path" - _config add "$repo_path" + + git --git-dir="$HOME/.cfg" --work-tree="$HOME/.cfg" add "$repo_path" + echo "Added: $file_path -> $repo_path" done ;; @@ -187,7 +309,6 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then local rm_opts="" local file_path_list=() - # Separate options from file paths for arg in "$@"; do if [[ "$arg" == "-"* ]]; then rm_opts+=" $arg" @@ -199,16 +320,13 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then for file_path in "${file_path_list[@]}"; do local repo_path="$(_repo_path "$file_path")" - # Use a dummy run of `git rm` to handle the recursive flag if [[ "$rm_opts" == *"-r"* ]]; then _config rm --cached -r "$repo_path" else _config rm --cached "$repo_path" fi - # Remove from the filesystem, passing the collected options eval "rm $rm_opts \"$file_path\"" - echo "Removed: $file_path" done ;; @@ -218,12 +336,12 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then local sys_file="$(_sys_path "$repo_file")" local full_repo_path="$HOME/.cfg/$repo_file" if [[ "$direction" == "to-repo" ]]; then - if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file")" ]]; then + if [[ -e "$sys_file" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then cp -a "$sys_file" "$full_repo_path" echo "Synced to repo: $sys_file" fi elif [[ "$direction" == "from-repo" ]]; then - if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file")" ]]; then + if [[ -e "$full_repo_path" && -n "$(diff "$full_repo_path" "$sys_file" 2>/dev/null || echo "diff")" ]]; then local dest_dir="$(dirname "$sys_file")" if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then _sudo_prompt mkdir -p "$dest_dir" @@ -238,14 +356,13 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then done ;; status) - # Auto-sync any modified files local auto_synced=() while read -r repo_file; do local sys_file="$(_sys_path "$repo_file")" local full_repo_path="$HOME/.cfg/$repo_file" if [[ -e "$sys_file" && -e "$full_repo_path" ]]; then if ! diff -q "$full_repo_path" "$sys_file" >/dev/null 2>&1; then - \cp -fa "$sys_file" "$full_repo_path" + cp -fa "$sys_file" "$full_repo_path" auto_synced+=("$repo_file") fi fi @@ -262,20 +379,47 @@ if [[ -d "$HOME/.cfg" && -d "$HOME/.cfg/refs" ]]; then ;; deploy) _config ls-files | while read -r repo_file; do - local sys_file="$(_sys_path "$repo_file")" local full_repo_path="$HOME/.cfg/$repo_file" - if [[ -e "$full_repo_path" ]]; then - if [[ -n "$sys_file" ]]; then - local dest_dir="$(dirname "$sys_file")" - if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then - _sudo_prompt mkdir -p "$dest_dir" - _sudo_prompt cp -a "$full_repo_path" "$sys_file" - else - mkdir -p "$dest_dir" - cp -a "$full_repo_path" "$sys_file" - fi - echo "Deployed: $repo_file -> $sys_file" + local sys_file="$(_sys_path "$repo_file")" # destination only + + # Only continue if the source exists + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if needed + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" + fi + + echo "Deployed: $repo_file -> $sys_file" + fi + done + ;; + checkout) + echo "Checking out dotfiles from .cfg..." + _config ls-files | while read -r repo_file; do + local full_repo_path="$HOME/.cfg/$repo_file" + local sys_file="$(_sys_path "$repo_file")" + + if [[ -e "$full_repo_path" && -n "$sys_file" ]]; then + local dest_dir + dest_dir="$(dirname "$sys_file")" + + # Create destination if it doesn't exist + if [[ "$sys_file" == /* && "$sys_file" != "$HOME/"* ]]; then + _sudo_prompt mkdir -p "$dest_dir" + _sudo_prompt cp -a "$full_repo_path" "$sys_file" + else + mkdir -p "$dest_dir" + cp -a "$full_repo_path" "$sys_file" fi + + echo "Checked out: $repo_file -> $sys_file" fi done ;; @@ -560,14 +704,39 @@ if (Test-Path "$HOME\.cfg" -and Test-Path "$HOME\.cfg\refs") { Restart the terminal or source the session profile file used. -4. Make sure to not show untracked files +4. Checkout dotfiles from the repository + +**Important:** After cloning the bare repository, you need to checkout the files to restore the directory structure: ```bash -config config --local status.showUntrackedFiles no +# Linux/MacOS/WSL +config checkout +``` + +```ps1 +# Windows (PowerShell) +config checkout +``` + +If you get conflicts about existing files, you can force the checkout: + +```bash +# Linux/MacOS/WSL +config checkout -f +``` + +```ps1 +# Windows (PowerShell) +config checkout -f ``` +5. Configure repository settings + +```bash +config config --local status.showUntrackedFiles no +``` -5. Deploy dotfiles +6. Deploy dotfiles to system locations ```bash config deploy @@ -578,34 +747,23 @@ config deploy ### Auto-installer -Linux/MacOS: +Linux/macOS (one-liner): -```bash -wget -q "https://github.com/srdusr/dotfiles/archive/main.tar.gz" -O "$HOME/Downloads/dotfiles.tar.gz" -mkdir -p "$HOME/dotfiles-main" -tar -xf "$HOME/Downloads/dotfiles.tar.gz" -C "$HOME/dotfiles-main" --strip-components=1 -mv -f "$HOME/dotfiles-main/"* "$HOME" -rm -rf "$HOME/dotfiles-main" -chmod +x "$HOME/install.sh" -rm "$HOME/Downloads/dotfiles.tar.gz" -$HOME/install.sh +```sh +sh -c "$(curl -fsSL https://raw.githubusercontent.com/srdusr/dotfiles/main/common/install.sh)" ``` -Windows: +Windows PowerShell (one-liner): -```ps1 -Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force; ` -$ProgressPreference = 'SilentlyContinue'; ` -Invoke-WebRequest "https://github.com/srdusr/dotfiles/archive/main.zip" ` --OutFile "$HOME\Downloads\dotfiles.zip"; ` -Expand-Archive -Path "$HOME\Downloads\dotfiles.zip" -DestinationPath "$HOME" -Force; ` -Move-Item -Path "$HOME\dotfiles-main\*" -Destination "$HOME" -Force; ` -Remove-Item -Path "$HOME\dotfiles-main" -Recurse -Force; ` -. "$HOME\install.bat" +```powershell +Set-ExecutionPolicy Bypass -Scope Process -Force; irm 'https://raw.githubusercontent.com/srdusr/dotfiles/main/windows/Documents/PowerShell/bootstrap.ps1' | iex +``` +Windows CMD (.bat alternative): -Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force -irm 'https://raw.githubusercontent.com/srdusr/dotfiles/main/windows/Documents/PowerShell/bootstrap.ps1' | iex +```bat +REM From the cloned repo, run the batch installer (if present): +call windows\Documents\install.bat ``` --- @@ -663,7 +821,7 @@ To add a file specific to your operating system: ```bash # Bash/Zsh: -config add .bashrc # This is added to $HOME/.cfg/linux/home/.bashrc +config add --target common .bashrc # Added to $HOME/.cfg/common/.bashrc ``` ```bash diff --git a/common/install.sh b/common/install.sh index a12c258..4a2b209 100755 --- a/common/install.sh +++ b/common/install.sh @@ -4,6 +4,8 @@ # Created On: Tue 06 Sep 2025 16:20:52 PM CAT # Project: Dotfiles installation script +# TODO: allow optional change user/password, also optional change root password, first check if they are the same (auto) + # Dependencies: git, curl set -euo pipefail # Exit on error, undefined vars, pipe failures @@ -535,7 +537,8 @@ detect_package_manager() { if command_exists yq && [[ -n "$found_packages_file" ]]; then # Prefer distro block, fallback to manager block - local pm_update pm_install + # Initialize to avoid set -u (nounset) issues before assignment + local pm_update="" pm_install="" if [[ -n "$DISTRO" ]]; then pm_update=$(yq eval ".package_managers.${DISTRO}.update" "$found_packages_file" 2>/dev/null | grep -v "^null$" || echo "") pm_install=$(yq eval ".package_managers.${DISTRO}.install" "$found_packages_file" 2>/dev/null | grep -v "^null$" || echo "") @@ -551,6 +554,9 @@ detect_package_manager() { fi fi + # Export for compatibility with packages.yml custom commands that reference CFG_DISTRO + export CFG_DISTRO="$DISTRO" + mark_step_completed "detect_package_manager" return 0 else @@ -1194,8 +1200,6 @@ check_existing_config_command() { CONFIG_COMMAND_AVAILABLE=true CONFIG_COMMAND_FILE="$f" print_success "Config command found in: $f" - # Do NOT source user shell files here to avoid early exits or side-effects. - # We'll rely on fallbacks (git/manual deploy) if the function is not in the current shell. return 0 fi fi @@ -1623,7 +1627,8 @@ deploy_config() { else # Fallback: use git directly print_info "Using git directly to checkout files..." - if git --git-dir="$DOTFILES_DIR" --work-tree="$DOTFILES_DIR" checkout HEAD -- . 2>/dev/null; then + # IMPORTANT: use $HOME/.cfg as work-tree, never the bare repo path + if git --git-dir="$DOTFILES_DIR" --work-tree="$HOME/.cfg" checkout HEAD -- . 2>/dev/null; then print_success "Files checked out using git directly" else print_warning "Git checkout had issues, continuing anyway..." @@ -1679,13 +1684,24 @@ deploy_config() { } verify_config_command() { + # Always verify the function is actually available in this shell if type config >/dev/null 2>&1; then + CONFIG_COMMAND_AVAILABLE=true print_success "Config command is available and working" return 0 - else - print_warning "Config command not available" - return 1 fi + # Try sourcing the detected profile file if known + if [[ -n "$CONFIG_COMMAND_FILE" && -f "$CONFIG_COMMAND_FILE" ]]; then + # shellcheck disable=SC1090 + source "$CONFIG_COMMAND_FILE" 2>/dev/null || true + if type config >/dev/null 2>&1; then + CONFIG_COMMAND_AVAILABLE=true + print_success "Config command is available and working" + return 0 + fi + fi + print_warning "Config command not available" + return 1 } # Manual deployment function (fallback when config command not available) @@ -1697,8 +1713,9 @@ manual_deploy_dotfiles() { return 1 fi - local os_dir="$DOTFILES_DIR/$CFG_OS" - local common_dir="$DOTFILES_DIR/common" + # Source locations are always within the checked-out work-tree ($HOME/.cfg) + local os_dir="$HOME/.cfg/$CFG_OS" + local common_dir="$HOME/.cfg/common" deploy_file() { local repo_file="$1" @@ -1730,7 +1747,8 @@ manual_deploy_dotfiles() { esac ;; common/assets/*) - sys_file="$HOME/.cfg/$rel_path" + # Assets are repo-internal; do not deploy to filesystem + return 0 ;; common/*) sys_file="$HOME/${rel_path#common/}" @@ -1750,6 +1768,15 @@ manual_deploy_dotfiles() { sys_dir="$(dirname "$sys_file")" mkdir -p "$sys_dir" + # Avoid copying if source and destination resolve to the same file + local src_real dst_real + src_real=$(readlink -f -- "$repo_file" 2>/dev/null || echo "$repo_file") + dst_real=$(readlink -f -- "$sys_file" 2>/dev/null || echo "$sys_file") + if [[ -n "$dst_real" && "$src_real" == "$dst_real" ]]; then + print_skip "Skipping self-copy: $rel_path" + return 0 + fi + # Copy with privilege if path is system (/etc, /usr, etc.) if [[ "$sys_file" == /* ]]; then # If we lack a privilege tool and are not root, skip with clear message @@ -1884,13 +1911,146 @@ install_dotfiles() { if [[ -d "$DOTFILES_DIR" ]]; then if [[ "$UPDATE_MODE" == true ]] || prompt_user "Dotfiles repository already exists. Update it?"; then print_info "Updating existing dotfiles..." - if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + # Detect ahead/behind before pulling to avoid unexpected fast-forwards + execute_command "git --git-dir='$DOTFILES_DIR' fetch origin main" || true + local ahead behind ab_line + ahead=0; behind=0 + ab_line=$(git --git-dir="$DOTFILES_DIR" rev-list --left-right --count HEAD...origin/main 2>/dev/null || true) + # Expected format: "\t"; parse safely + if [[ "$ab_line" =~ ^([0-9]+)[[:space:]]+([0-9]+)$ ]]; then + ahead="${BASH_REMATCH[1]}" + behind="${BASH_REMATCH[2]}" + fi + if [[ ${ahead:-0} -gt 0 && ${behind:-0} -eq 0 ]]; then + print_warning "Your local dotfiles are ahead of origin/main by $ahead commit(s)." + while true; do + echo + print_color "$YELLOW" "Choose an action for local-ahead state:" + echo " [k] Keep local (skip pull)" + echo " [p] Push local commits" + echo " [c] Commit new changes and push" + echo " [s] Stash uncommitted changes (if any) and pull" + echo " [a] Abort" + printf "%b%s%b" "$YELLOW" "Enter choice [k/p/c/s/a]: " "$NOCOLOR" + read -r choice + case "${choice,,}" in + k) + print_warning "Keeping local commits; skipping pull" + break + ;; + p) + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' push origin HEAD:main"; then + print_success "Pushed local commits" + else + print_error "Push failed" + fi + break + ;; + c) + print_info "Committing changes before push..." + printf "%b%s%b" "$YELLOW" "Commit message (default: 'WIP local changes via installer'): " "$NOCOLOR" + read -r commit_msg + [[ -z "$commit_msg" ]] && commit_msg="WIP local changes via installer" + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' add -A" \ + && execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' commit -m \"$commit_msg\"" \ + && execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' push origin HEAD:main"; then + print_success "Committed and pushed" + else + print_error "Commit/push failed" + fi + break + ;; + s) + print_info "Stashing local (including untracked) before pull..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' stash push -u -m 'installer-stash'"; then + print_success "Stashed local changes" + else + print_error "Stash failed" + fi + break + ;; + a) + print_error "Aborted by user" + mark_step_failed "install_dotfiles" + return 1 + ;; + *) + print_warning "Invalid choice. Please enter k/p/c/s/a." + ;; + esac + done + fi + # If remote is ahead (fast-forward), ask the user before pulling + if [[ ${behind:-0} -gt 0 && ${ahead:-0} -eq 0 ]]; then + print_warning "Origin/main is ahead by $behind commit(s)." + if ! prompt_user "Fast-forward to origin/main now?"; then + print_skip "User chose not to fast-forward; skipping pull" + # Skip pull entirely + goto_after_pull=true + fi + fi + if [[ "${goto_after_pull:-false}" == true ]] || execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then update=true print_success "Dotfiles updated successfully" else print_error "Failed to pull updates" - mark_step_failed "install_dotfiles" - return 1 + # Interactive resolution for local changes + while true; do + echo + print_color "$YELLOW" "Local changes detected. Choose an action:" + echo " [c] Commit local changes" + echo " [s] Stash local changes" + echo " [k] Keep local changes (skip pulling)" + echo " [a] Abort" + printf "%b%s%b" "$YELLOW" "Enter choice [c/s/k/a]: " "$NOCOLOR" + read -r choice + case "${choice,,}" in + c) + print_info "Committing local changes..." + printf "%b%s%b" "$YELLOW" "Commit message (default: 'WIP local changes via installer'): " "$NOCOLOR" + read -r commit_msg + [[ -z "$commit_msg" ]] && commit_msg="WIP local changes via installer" + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' add -A" \ + && execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' commit -m \"$commit_msg\""; then + print_success "Committed local changes" + print_info "Retrying pull..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + update=true; print_success "Dotfiles updated successfully"; break + else + print_error "Pull failed again after commit. You may resolve manually or choose another option." + fi + else + print_error "Commit failed. Try another option." + fi + ;; + s) + print_info "Stashing local changes..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' stash push -u -m 'installer-stash'"; then + print_success "Stashed local changes" + print_info "Retrying pull..." + if execute_command "git --git-dir='$DOTFILES_DIR' --work-tree='$HOME/.cfg' pull origin main"; then + update=true; print_success "Dotfiles updated successfully"; break + else + print_error "Pull failed again after stash. You may resolve manually or choose another option." + fi + else + print_error "Stash failed. Try another option." + fi + ;; + k) + print_warning "Keeping local changes and skipping pull" + break + ;; + a) + print_error "Aborted by user" + mark_step_failed "install_dotfiles" + return 1 + ;; + *) + print_warning "Invalid choice. Please enter c/s/k/a." + ;; + esac + done fi else print_skip "Skipping dotfiles update" @@ -2234,16 +2394,60 @@ manage_service() { local action="$1" local service="$2" local init_system="$3" - local success=false + # use numeric success code: 0=success, 1=failure + local success=1 case "$init_system" in systemd) - if [ "$action" == "enable" ]; then - execute_command "$PRIVILEGE_TOOL systemctl enable '$service'" - success=$? - elif [ "$action" == "start" ]; then - execute_command "$PRIVILEGE_TOOL systemctl start '$service'" - success=$? + # Resolve common generic service names to distro-specific systemd unit names + local svc_candidates=() + local lower_service + lower_service="${service,,}" + case "$lower_service" in + networkmanager) + svc_candidates+=("NetworkManager" "NetworkManager.service" "network-manager") + ;; + sshd) + # Debian uses 'ssh' service, others commonly use 'sshd' + svc_candidates+=("sshd" "ssh" "sshd.service" "ssh.service") + ;; + *) + svc_candidates+=("$service") + ;; + esac + + local tried=false + local rc=1 + for svc in "${svc_candidates[@]}"; do + tried=true + if [ "$action" == "enable" ]; then + # Prefer enabling and starting in one go when possible + if ! execute_command "$PRIVILEGE_TOOL systemctl enable --now '$svc'"; then + execute_command "$PRIVILEGE_TOOL systemctl enable '$svc'" + fi + rc=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL systemctl start '$svc'" + rc=$? + else + rc=1 + fi + if [[ $rc -eq 0 ]]; then + success=0 + break + fi + print_warning "Failed to $action service candidate: $svc" + done + # If we didn't have a special mapping, fall back to original name once + if [[ "$tried" == false ]]; then + if [ "$action" == "enable" ]; then + execute_command "$PRIVILEGE_TOOL systemctl enable '$service'" + rc=$? + elif [ "$action" == "start" ]; then + execute_command "$PRIVILEGE_TOOL systemctl start '$service'" + rc=$? + fi + [[ $rc -eq 0 ]] && success=0 fi ;; openrc) @@ -2289,7 +2493,7 @@ manage_service() { ;; esac - return $((1 - success)) + return $success } #====================================== @@ -2936,8 +3140,6 @@ apply_linux_tweaks() { fi # Desktop environment tweaks should be declared in packages.yml under system_tweaks. - # This function keeps only essential, non-DE specific items. Use apply_system_tweaks - # to apply YAML-driven commands. print_info "Linux system tweaks applied (core). Desktop tweaks come from packages.yml." } @@ -2970,10 +3172,18 @@ handle_custom_installs() { condition=$(yq eval ".custom_installs.$install_name.condition" "$packages_file" 2>/dev/null | grep -v "^null$" || echo "") if [[ -n "$condition" ]]; then + # Evaluate condition safely even under set -u (nounset) + local -i _had_nounset=0 + if set -o | grep -q "nounset\s*on"; then + _had_nounset=1 + set +u + fi if ! eval "$condition" 2>/dev/null; then + if [[ $_had_nounset -eq 1 ]]; then set -u; fi print_info "Skipping $install_name (condition not met)" continue fi + if [[ $_had_nounset -eq 1 ]]; then set -u; fi fi # Get OS-specific command @@ -2999,6 +3209,13 @@ handle_custom_installs() { print_info "Running custom install: $install_name" if execute_command "$install_cmd"; then print_success "Custom install completed: $install_name" + # If yq was installed into ~/.local/bin via custom install, ensure PATH includes it for current session + if [[ "$install_name" == "yq" && -x "$HOME/.local/bin/yq" ]]; then + if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" + print_info "Added $HOME/.local/bin to PATH for current session" + fi + fi else print_error "Custom install failed: $install_name" fi diff --git a/common/packages.yml b/common/packages.yml index afc4504..a0f7d2f 100644 --- a/common/packages.yml +++ b/common/packages.yml @@ -804,10 +804,11 @@ custom_installs: yq: condition: "! command -v yq" linux: | + mkdir -p "$HOME/.local/bin" YQ_VERSION=$(curl -s https://api.github.com/repos/mikefarah/yq/releases/latest | grep 'tag_name' | cut -d'"' -f4) YQ_BINARY="yq_linux_amd64" - curl -L "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" -o ~/.local/bin/yq - chmod +x ~/.local/bin/yq + curl -L "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/${YQ_BINARY}" -o "$HOME/.local/bin/yq" + chmod +x "$HOME/.local/bin/yq" macos: "brew install yq" windows: "choco install yq" -- cgit v1.2.3