aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md294
-rwxr-xr-xcommon/install.sh263
-rw-r--r--common/packages.yml5
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 <b><i>$HOME</i></b>
- 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/\<User\>/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: "<ahead>\t<behind>"; 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"