diff --git a/.doom.d/config.el b/.doom.d/config.el
index f60eadba..81d731cd 100644
--- a/.doom.d/config.el
+++ b/.doom.d/config.el
@@ -138,15 +138,10 @@
"C-+" #'text-scale-increase ;; this was reset font size lol
)
- (:after evil
+ (:after evil-org
:map evil-org-mode-map
- :m "g j" nil
- :m "g k" nil
-;; (evil-global-set-key 'motion "j" 'evil-next-visual-line) ;; both of these
-;; (evil-global-set-key 'motion "k" 'evil-previous-visual-line) ;; are needed for org mode where g-j doesn't work properly
-
- ;; :desc "go down visual line" "g j" nil
- ;; :desc "go down visual line" "g k" nil
+ :n "g j" #'evil-next-visual-line
+ :n "g k" #'evil-previous-visual-line
)
(:leader
:desc "ws 0" "0" #'eyebrowse-switch-to-window-config-0
diff --git a/.github/assets/os-icons/arch.png b/.github/assets/os-icons/arch.png
new file mode 100644
index 00000000..a97ab4c6
Binary files /dev/null and b/.github/assets/os-icons/arch.png differ
diff --git a/.github/assets/os-icons/gentoo.png b/.github/assets/os-icons/gentoo.png
new file mode 100644
index 00000000..648c4292
Binary files /dev/null and b/.github/assets/os-icons/gentoo.png differ
diff --git a/.github/assets/os-icons/macos.png b/.github/assets/os-icons/macos.png
new file mode 100644
index 00000000..5485aa19
Binary files /dev/null and b/.github/assets/os-icons/macos.png differ
diff --git a/.github/assets/os-icons/manjaro.png b/.github/assets/os-icons/manjaro.png
new file mode 100644
index 00000000..9cbf75ac
Binary files /dev/null and b/.github/assets/os-icons/manjaro.png differ
diff --git a/.github/assets/os-icons/ubuntu.png b/.github/assets/os-icons/ubuntu.png
new file mode 100644
index 00000000..ab0742ef
Binary files /dev/null and b/.github/assets/os-icons/ubuntu.png differ
diff --git a/.local/scripts/bootstrap/arch b/.local/scripts/bootstrap/arch
index 8dadd8b7..2a84c401 100755
--- a/.local/scripts/bootstrap/arch
+++ b/.local/scripts/bootstrap/arch
@@ -3,9 +3,11 @@
set -euo pipefail
### VARIABLES
-scripts="$HOME/.local/scripts"
-source "$scripts/bootstrap/base_functions"
-source "$scripts/bootstrap/arch_functions"
+bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOTFILES_ROOT="$(cd "$bootstrap_dir/../../.." && pwd)"
+export DOTFILES_ROOT
+source "$bootstrap_dir/base_functions"
+source "$bootstrap_dir/arch_functions"
parse_bootstrap_args "$@"
configure_dual_boot_utc_rtc
@@ -16,6 +18,8 @@ resolve_first_run_choice
info "Updating system..."
sudo pacman -Syu
+bootstrap_stow_checkout arch
+
install_preparation
echo "#! /bin/sh
diff --git a/.local/scripts/bootstrap/arch_functions b/.local/scripts/bootstrap/arch_functions
index 39599ca5..f957597f 100644
--- a/.local/scripts/bootstrap/arch_functions
+++ b/.local/scripts/bootstrap/arch_functions
@@ -11,7 +11,7 @@
#npm by default is system-wide unless using npm_config_prefix then npm install packageName will install package in user
### VARIABLES
-scripts="$HOME/.local/scripts"
+scripts="${DOTFILES_ROOT:-$HOME}/.local/scripts"
flags="-S --noconfirm --needed"
pac="sudo pacman $flags"
yay="yay $flags"
diff --git a/.local/scripts/bootstrap/base_functions b/.local/scripts/bootstrap/base_functions
index c0ee94ce..16027011 100644
--- a/.local/scripts/bootstrap/base_functions
+++ b/.local/scripts/bootstrap/base_functions
@@ -11,6 +11,7 @@ error() { printf '\033[1;31m[ERROR]\033[0m %s\n' "$*" >&2; }
parse_bootstrap_args() {
DUAL_BOOT_UTC=false
UNATTENDED=false
+ SKIP_STOW=false
FIRST_RUN_CHOICE=""
HIGH_DPI_CHOICE=""
@@ -22,6 +23,9 @@ parse_bootstrap_args() {
--unattended)
UNATTENDED=true
;;
+ --no-stow)
+ SKIP_STOW=true
+ ;;
--first-run=yes|--first-run=no)
FIRST_RUN_CHOICE="${1#*=}"
;;
@@ -36,7 +40,113 @@ parse_bootstrap_args() {
shift
done
- export DUAL_BOOT_UTC UNATTENDED FIRST_RUN_CHOICE HIGH_DPI_CHOICE
+ export DUAL_BOOT_UTC UNATTENDED SKIP_STOW FIRST_RUN_CHOICE HIGH_DPI_CHOICE
+}
+
+## Return the dotfiles checkout root used by this bootstrap invocation.
+bootstrap_repository_root() {
+ if [ -n "${DOTFILES_ROOT:-}" ] && [ -d "$DOTFILES_ROOT" ]; then
+ printf '%s\n' "$DOTFILES_ROOT"
+ return 0
+ fi
+
+ cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd
+}
+
+## Report whether this bootstrap invocation can obtain a confirmation response.
+bootstrap_stdin_is_interactive() {
+ [ -t 0 ] && [ -t 1 ]
+}
+
+## Ask whether to apply the displayed Stow and conflict-backup plan.
+bootstrap_confirm_stow() {
+ local answer
+ read -r -p "Apply the displayed Stow plan? [y/N] " answer
+ [[ "$answer" =~ ^([yY][eE][sS]|[yY])$ ]]
+}
+
+## Return whether GNU Stow is available on the current PATH.
+bootstrap_stow_is_available() {
+ command -v stow >/dev/null 2>&1
+}
+
+## Install GNU Stow through the package manager for the selected profile.
+bootstrap_install_stow() {
+ local profile="$1"
+
+ info "GNU Stow is required to link this checkout; installing it for $profile..."
+ case "$profile" in
+ ubuntu|ubuntu-windows|work)
+ sudo apt-get install -y stow
+ ;;
+ arch|manjaro)
+ sudo pacman -S --noconfirm --needed stow
+ ;;
+ mac)
+ if ! command -v brew >/dev/null 2>&1; then
+ if declare -F brew_install >/dev/null 2>&1; then
+ brew_install
+ else
+ error "Homebrew is required to install GNU Stow on macOS."
+ return 1
+ fi
+ fi
+ brew install stow
+ ;;
+ *)
+ error "No GNU Stow installer is configured for bootstrap profile: $profile"
+ return 2
+ ;;
+ esac
+
+ if ! bootstrap_stow_is_available; then
+ error "GNU Stow installation did not make the stow command available."
+ return 1
+ fi
+}
+
+## Preview and, with confirmation, link the current checkout through GNU Stow.
+bootstrap_stow_checkout() {
+ local profile="$1"
+ local dotfiles_root parent_dir package_name preview
+
+ if [ "${SKIP_STOW:-false}" = true ]; then
+ info "Skipping Stow linking (--no-stow); continuing from the checkout paths."
+ return 0
+ fi
+
+ if [ "${UNATTENDED:-false}" = true ] || ! bootstrap_stdin_is_interactive; then
+ warn "Stow linking was not applied because confirmation is unavailable. Continuing from checkout paths; rerun interactively or pass --no-stow."
+ return 0
+ fi
+
+ if ! bootstrap_stow_is_available; then
+ bootstrap_install_stow "$profile"
+ fi
+
+ dotfiles_root="$(bootstrap_repository_root)"
+ parent_dir="$(dirname "$dotfiles_root")"
+ package_name="$(basename "$dotfiles_root")"
+ preview="$(stow -v -n -t "$HOME" -d "$parent_dir" "$package_name" 2>&1 || true)"
+
+ info "Stow dry run for $dotfiles_root:"
+ if [ -n "$preview" ]; then
+ printf '%s\n' "$preview"
+ else
+ info "No Stow changes are needed."
+ return 0
+ fi
+
+ info "Conflict backup plan:"
+ bash "$dotfiles_root/.local/scripts/stow-backup-conflicts" --dry-run
+
+ if ! bootstrap_confirm_stow; then
+ info "Stow linking declined; continuing from checkout paths."
+ return 0
+ fi
+
+ bash "$dotfiles_root/.local/scripts/stow-backup-conflicts"
+ stow -v -t "$HOME" -d "$parent_dir" "$package_name"
}
resolve_first_run_choice() {
@@ -148,6 +258,7 @@ configure_dual_boot_utc_rtc() {
install_preparation() {
info "Preparing environment..."
+ mkdir -p "$HOME/.vim"
echo "colorscheme onedark
set background=dark" > "$HOME/.vim/theme"
diff --git a/.local/scripts/bootstrap/mac b/.local/scripts/bootstrap/mac
index 373a914f..40840cfe 100644
--- a/.local/scripts/bootstrap/mac
+++ b/.local/scripts/bootstrap/mac
@@ -3,16 +3,19 @@
set -euo pipefail
### VARIABLES
-scripts="$HOME/.local/scripts"
-source "$scripts/bootstrap/base_functions"
-source "$scripts/bootstrap/mac_functions"
+bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOTFILES_ROOT="$(cd "$bootstrap_dir/../../.." && pwd)"
+export DOTFILES_ROOT
+source "$bootstrap_dir/base_functions"
+source "$bootstrap_dir/mac_functions"
parse_bootstrap_args "$@"
configure_dual_boot_utc_rtc
### INSTALLATION
+brew_install
+bootstrap_stow_checkout mac
install_preparation
#app_store_install
-brew_install
mac_install
ai_tools_install
vim_install
diff --git a/.local/scripts/bootstrap/mac_functions b/.local/scripts/bootstrap/mac_functions
index ece9feb8..c3b4aba0 100755
--- a/.local/scripts/bootstrap/mac_functions
+++ b/.local/scripts/bootstrap/mac_functions
@@ -183,7 +183,7 @@ vim_install () {
cd "$HOME" || exit
vim +PlugInstall +qall
# Vim YouCompleteMe plugin install
- bash "$HOME/.local/scripts/ycm.sh"
+ bash "${DOTFILES_ROOT:-$HOME}/.local/scripts/ycm.sh"
sudo npm install -g neovim
python3 -m pip install --user --upgrade pynvim
sudo gem install neovim
diff --git a/.local/scripts/bootstrap/manjaro b/.local/scripts/bootstrap/manjaro
index ebd099bb..d7c0783b 100755
--- a/.local/scripts/bootstrap/manjaro
+++ b/.local/scripts/bootstrap/manjaro
@@ -5,9 +5,11 @@ set -euo pipefail
### WARNING: If this script is to be changed, beware of the order of execution of every command
### VARIABLES
-scripts="$HOME/.local/scripts"
-source "$scripts/bootstrap/base_functions"
-source "$scripts/bootstrap/arch_functions"
+bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOTFILES_ROOT="$(cd "$bootstrap_dir/../../.." && pwd)"
+export DOTFILES_ROOT
+source "$bootstrap_dir/base_functions"
+source "$bootstrap_dir/arch_functions"
parse_bootstrap_args "$@"
configure_dual_boot_utc_rtc
@@ -18,6 +20,8 @@ resolve_first_run_choice
info "Updating system..."
sudo pacman -Syu
+bootstrap_stow_checkout manjaro
+
install_preparation
echo "#! /bin/sh
diff --git a/.local/scripts/bootstrap/ubuntu b/.local/scripts/bootstrap/ubuntu
index a9940a47..47359232 100755
--- a/.local/scripts/bootstrap/ubuntu
+++ b/.local/scripts/bootstrap/ubuntu
@@ -3,9 +3,11 @@
set -euo pipefail
### VARIABLES
-scripts="$HOME/.local/scripts"
-source "$scripts/bootstrap/base_functions"
-source "$scripts/bootstrap/ubuntu_functions"
+bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOTFILES_ROOT="$(cd "$bootstrap_dir/../../.." && pwd)"
+export DOTFILES_ROOT
+source "$bootstrap_dir/base_functions"
+source "$bootstrap_dir/ubuntu_functions"
parse_bootstrap_args "$@"
configure_dual_boot_utc_rtc
@@ -16,6 +18,8 @@ resolve_first_run_choice
info "Updating system..."
sudo apt-get update && sudo apt-get full-upgrade -y
+bootstrap_stow_checkout ubuntu
+
install_preparation
echo "#! /bin/sh
diff --git a/.local/scripts/bootstrap/ubuntu_functions b/.local/scripts/bootstrap/ubuntu_functions
index da4485c8..b55fd817 100644
--- a/.local/scripts/bootstrap/ubuntu_functions
+++ b/.local/scripts/bootstrap/ubuntu_functions
@@ -2,7 +2,7 @@
# Author: https://github.com/cuberhaus
flags="-y"
apt="sudo apt-get $flags install"
-ycm="$HOME/.local/scripts/ycm.sh" # make sure it's there
+ycm="${DOTFILES_ROOT:-$HOME}/.local/scripts/ycm.sh" # make sure it's there
scripts="$HOME/.local/scripts"
base_install () {
diff --git a/.local/scripts/bootstrap/ubuntu_windows b/.local/scripts/bootstrap/ubuntu_windows
index 5ce6d7f5..402fb2de 100755
--- a/.local/scripts/bootstrap/ubuntu_windows
+++ b/.local/scripts/bootstrap/ubuntu_windows
@@ -1,18 +1,19 @@
#! /usr/bin/env bash
# Author: https://github.com/cuberhaus
-### First deploy dotfiles with stow, then execute scripts
-
### VARIABLES
-scripts="$HOME/.local/scripts"
+bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOTFILES_ROOT="$(cd "$bootstrap_dir/../../.." && pwd)"
+export DOTFILES_ROOT
# Include functions
-source "$scripts/bootstrap/ubuntu_functions"
-source "$scripts/bootstrap/base_functions"
+source "$bootstrap_dir/ubuntu_functions"
+source "$bootstrap_dir/base_functions"
parse_bootstrap_args "$@"
configure_dual_boot_utc_rtc
### INSTALLATION
sudo apt-get update; sudo apt-get full-upgrade # Update
+bootstrap_stow_checkout ubuntu-windows
install_preparation
echo "#! /bin/sh
export DISTRO=ubuntu_windows" > "$HOME/.config/distro"
diff --git a/.local/scripts/bootstrap/work b/.local/scripts/bootstrap/work
index 9d6add24..a63ac7c3 100755
--- a/.local/scripts/bootstrap/work
+++ b/.local/scripts/bootstrap/work
@@ -4,9 +4,11 @@
set -euo pipefail
### VARIABLES
-scripts="$HOME/.local/scripts"
-source "$scripts/bootstrap/base_functions"
-source "$scripts/bootstrap/work_functions"
+bootstrap_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DOTFILES_ROOT="$(cd "$bootstrap_dir/../../.." && pwd)"
+export DOTFILES_ROOT
+source "$bootstrap_dir/base_functions"
+source "$bootstrap_dir/work_functions"
parse_bootstrap_args "$@"
configure_dual_boot_utc_rtc
@@ -14,6 +16,8 @@ configure_dual_boot_utc_rtc
info "Updating system..."
sudo apt-get update && sudo apt-get full-upgrade -y
+bootstrap_stow_checkout work
+
install_preparation
echo "#! /bin/sh
diff --git a/.local/scripts/bootstrap/work_functions b/.local/scripts/bootstrap/work_functions
index eda5d246..ba8b8cc0 100644
--- a/.local/scripts/bootstrap/work_functions
+++ b/.local/scripts/bootstrap/work_functions
@@ -9,7 +9,7 @@ apt="sudo apt-get install -y"
###############################################################
shutdown_fix() {
- sudo bash "$HOME/.local/scripts/permanent_shutdown_fix.sh"
+ sudo bash "${DOTFILES_ROOT:-$HOME}/.local/scripts/permanent_shutdown_fix.sh"
}
###############################################################
diff --git a/.local/scripts/stow-backup-conflicts b/.local/scripts/stow-backup-conflicts
index 8f7f9d2f..12264026 100755
--- a/.local/scripts/stow-backup-conflicts
+++ b/.local/scripts/stow-backup-conflicts
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# stow-backup-conflicts — detect stow conflicts and back them up
#
-# Usage: stow-backup-conflicts [backup-dir]
+# Usage: stow-backup-conflicts [--dry-run] [backup-dir]
#
# Runs a stow dry-run, parses conflicts, and moves each conflicting file
# into a timestamped backup folder. Exits 0 if there were no conflicts
@@ -25,6 +25,13 @@ TARGET="${HOME}"
BACKUP_ROOT="${1:-$HOME/.dotfiles-backup}"
BACKUP_DIR="$BACKUP_ROOT/$(date '+%Y-%m-%d_%H%M%S')"
+DRY_RUN=false
+
+if [ "${1:-}" = "--dry-run" ]; then
+ DRY_RUN=true
+ BACKUP_ROOT="${2:-$HOME/.dotfiles-backup}"
+ BACKUP_DIR="$BACKUP_ROOT/$(date '+%Y-%m-%d_%H%M%S')"
+fi
# ---------------------------------------------------------------------------
# Detect conflicts via stow dry-run
@@ -47,8 +54,12 @@ if (( ${#conflicts[@]} == 0 )); then
exit 0
fi
-info "Found ${#conflicts[@]} conflict(s). Backing up to $BACKUP_DIR"
-mkdir -p "$BACKUP_DIR"
+if [ "$DRY_RUN" = true ]; then
+ info "Found ${#conflicts[@]} conflict(s). Would back up to $BACKUP_DIR"
+else
+ info "Found ${#conflicts[@]} conflict(s). Backing up to $BACKUP_DIR"
+ mkdir -p "$BACKUP_DIR"
+fi
for rel in "${conflicts[@]}"; do
src="$TARGET/$rel"
@@ -59,8 +70,17 @@ for rel in "${conflicts[@]}"; do
continue
fi
+ if [ "$DRY_RUN" = true ]; then
+ info " Would move $src to $dest"
+ continue
+ fi
+
mkdir -p "$(dirname "$dest")"
mv -v "$src" "$dest"
done
-ok "All conflicts backed up to $BACKUP_DIR"
+if [ "$DRY_RUN" = true ]; then
+ ok "No files were moved."
+else
+ ok "All conflicts backed up to $BACKUP_DIR"
+fi
diff --git a/Makefile b/Makefile
index 6ac57990..69dddc6c 100644
--- a/Makefile
+++ b/Makefile
@@ -73,6 +73,7 @@ lint: ## Run shellcheck on all shell scripts
test: ## Run deterministic unit tests
$(PYTHON) tests/test_installation_audit.py
+ bash tests/test_bootstrap_stow.sh
check: lint test ## Run tests and all linters (shellcheck + markdownlint + vint). Fails if any tool is missing.
@echo ""
diff --git a/README.md b/README.md
index f11131b8..90191889 100644
--- a/README.md
+++ b/README.md
@@ -60,6 +60,14 @@ The `$DOTFILES` variable (exported by `.zshenv`) points to the repo root,
auto-detected by resolving the `.zshenv` symlink. Scripts and configs that
need to reference the repo should use `$DOTFILES`.
+Bootstrap profiles also start directly from their running checkout. Before
+linking any files, they install GNU Stow through the selected platform when
+needed, show the Stow dry run and any conflict-backup plan, and ask for an
+explicit confirmation. Declining continues provisioning through checkout paths
+without linking files. Pass `BOOTSTRAP_ARGS="--no-stow"` to opt out explicitly;
+an unattended or noninteractive bootstrap also leaves Stow unchanged rather
+than assuming consent.
+
## Installation
Clone the repo with its submodules and use [GNU Stow](https://www.gnu.org/software/stow/) to symlink everything into `$HOME`:
@@ -208,6 +216,14 @@ Each bootstrap entrypoint follows the same pattern:
6. Switches the default shell to zsh.
7. Installs the native maintenance schedules shown below.
+### Doom Emacs Org visual-line movement
+
+After changing [.doom.d/config.el](.doom.d/config.el), run `doom sync`, then
+restart Doom Emacs (or evaluate the changed form). In an Org buffer with a
+soft-wrapped paragraph, `g j` and `g k` in Evil normal state move by displayed
+screen lines; a count such as `3 g j` moves three displayed lines. Confirm that
+the same keys retain their ordinary Evil behavior in a non-Org buffer.
+
The bootstrap installs these automations without requiring a separate command:
| Automation | Linux | macOS |
@@ -281,10 +297,10 @@ scripts directory.
## Supported OS
-- ![Arch\_icon][arch_icon] Arch
-- ![Manjaro\_icon][manjaro_icon] Manjaro
-- ![Ubuntu\_icon][ubuntu_icon] Ubuntu
-- ![MacOS\_icon][macos_icon] macOS
+-
Arch
+-
Manjaro
+-
Ubuntu
+-
macOS
## Window Managers
@@ -295,11 +311,5 @@ scripts directory.
## WIP
-- ![Gentoo\_icon][gentoo_icon] Gentoo
+-
Gentoo
- Openbox
-
-[manjaro_icon]: https://i.imgur.com/rfuvfYo.png
-[arch_icon]: https://upload.wikimedia.org/wikipedia/commons/a/a5/Archlinux-icon-crystal-64.svg
-[ubuntu_icon]: https://i.imgur.com/EX9n2Ib.png?1
-[macos_icon]: https://i.imgur.com/olG7ewE.png?1
-[gentoo_icon]: https://i.imgur.com/cKReKS2.png
diff --git a/tests/test_bootstrap_stow.sh b/tests/test_bootstrap_stow.sh
new file mode 100644
index 00000000..34c6efa7
--- /dev/null
+++ b/tests/test_bootstrap_stow.sh
@@ -0,0 +1,168 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+BASE_FUNCTIONS="$REPO_ROOT/.local/scripts/bootstrap/base_functions"
+ORIGINAL_PATH="$PATH"
+
+fail() {
+ printf 'FAIL: %s\n' "$*" >&2
+ exit 1
+}
+
+assert_file_contains() {
+ local file="$1"
+ local expected="$2"
+ grep -Fq -- "$expected" "$file" || fail "Expected $file to contain: $expected"
+}
+
+assert_file_missing() {
+ local file="$1"
+ [ ! -e "$file" ] || fail "Expected $file to be absent"
+}
+
+write_fake_stow() {
+ cat > "$FAKE_BIN/stow" <<'EOF'
+#!/usr/bin/env bash
+printf '%s\n' "$*" >> "$STOW_LOG"
+case " $* " in
+ *" -n "*) printf '%s\n' "${FAKE_STOW_PREVIEW:-}" ;;
+esac
+EOF
+ chmod +x "$FAKE_BIN/stow"
+}
+
+setup_case() {
+ CASE_DIR="$(mktemp -d)"
+ unset SKIP_STOW TEST_CONFIRM UNATTENDED
+ export HOME="$CASE_DIR/home"
+ export FAKE_BIN="$CASE_DIR/bin"
+ export STOW_LOG="$CASE_DIR/stow.log"
+ export DOTFILES_ROOT="$REPO_ROOT"
+ mkdir -p "$HOME" "$FAKE_BIN"
+ : > "$STOW_LOG"
+ write_fake_stow
+ export PATH="$FAKE_BIN:$ORIGINAL_PATH"
+}
+
+teardown_case() {
+ rm -rf "$CASE_DIR"
+ export PATH="$ORIGINAL_PATH"
+}
+
+run_stow_preflight() {
+ (
+ source "$BASE_FUNCTIONS"
+ bootstrap_stdin_is_interactive() { return 0; }
+ bootstrap_confirm_stow() { [ "${TEST_CONFIRM:-yes}" = yes ]; }
+ bootstrap_stow_checkout "$1"
+ )
+}
+
+test_clean_install_applies_previewed_links() {
+ setup_case
+ export FAKE_STOW_PREVIEW='LINK: .zshenv => dotfiles/.zshenv'
+ run_stow_preflight ubuntu
+ grep -Fv -- ' -n ' "$STOW_LOG" | grep -Fq -- ' -t ' \
+ || fail 'Expected Stow apply after confirmation'
+ teardown_case
+}
+
+test_existing_links_are_idempotent() {
+ setup_case
+ export FAKE_STOW_PREVIEW=''
+ run_stow_preflight ubuntu
+ if grep -Fv -- ' -n ' "$STOW_LOG" | grep -Fq -- ' -t '; then
+ fail 'Existing links should not trigger a Stow apply'
+ fi
+ teardown_case
+}
+
+test_conflicts_are_backed_up_after_confirmation() {
+ setup_case
+ export FAKE_STOW_PREVIEW='* existing target is neither a link nor a directory: .zshenv'
+ printf 'existing configuration\n' > "$HOME/.zshenv"
+ run_stow_preflight ubuntu
+ assert_file_missing "$HOME/.zshenv"
+ find "$HOME/.dotfiles-backup" -type f -name '.zshenv' -print -quit | grep -q . \
+ || fail 'Expected the conflicting file to be backed up'
+ teardown_case
+}
+
+test_decline_preserves_conflicts() {
+ setup_case
+ export FAKE_STOW_PREVIEW='* existing target is neither a link nor a directory: .zshenv'
+ export TEST_CONFIRM=no
+ printf 'existing configuration\n' > "$HOME/.zshenv"
+ run_stow_preflight ubuntu
+ [ -f "$HOME/.zshenv" ] || fail 'Declining must preserve the conflicting file'
+ assert_file_missing "$HOME/.dotfiles-backup"
+ teardown_case
+}
+
+test_no_stow_skips_all_stow_commands() {
+ setup_case
+ export SKIP_STOW=true
+ export FAKE_STOW_PREVIEW='LINK: .zshenv => dotfiles/.zshenv'
+ run_stow_preflight ubuntu
+ [ ! -s "$STOW_LOG" ] || fail '--no-stow must not invoke Stow'
+ teardown_case
+}
+
+test_no_stow_argument_sets_skip_flag() {
+ (
+ source "$BASE_FUNCTIONS"
+ parse_bootstrap_args --no-stow
+ [ "$SKIP_STOW" = true ] || fail '--no-stow must set SKIP_STOW=true'
+ )
+}
+
+test_missing_stow_uses_selected_platform_installer() {
+ setup_case
+ rm "$FAKE_BIN/stow"
+ export FAKE_STOW_PREVIEW='LINK: .zshenv => dotfiles/.zshenv'
+ (
+ source "$BASE_FUNCTIONS"
+ bootstrap_stdin_is_interactive() { return 0; }
+ bootstrap_confirm_stow() { return 0; }
+ bootstrap_stow_is_available() { [ -x "$FAKE_BIN/stow" ]; }
+ bootstrap_install_stow() {
+ printf 'install:%s\n' "$1" >> "$STOW_LOG"
+ write_fake_stow
+ }
+ bootstrap_stow_checkout ubuntu
+ )
+ assert_file_contains "$STOW_LOG" 'install:ubuntu'
+ teardown_case
+}
+
+test_noninteractive_input_never_applies_stow() {
+ setup_case
+ export FAKE_STOW_PREVIEW='LINK: .zshenv => dotfiles/.zshenv'
+ printf '' | (
+ source "$BASE_FUNCTIONS"
+ bootstrap_stow_checkout ubuntu
+ )
+ [ ! -s "$STOW_LOG" ] || fail 'Noninteractive input must not invoke Stow'
+ teardown_case
+}
+
+test_entrypoints_use_checkout_relative_paths() {
+ local entrypoint
+ for entrypoint in arch manjaro ubuntu ubuntu_windows mac work; do
+ assert_file_contains "$REPO_ROOT/.local/scripts/bootstrap/$entrypoint" 'DOTFILES_ROOT='
+ assert_file_contains "$REPO_ROOT/.local/scripts/bootstrap/$entrypoint" 'bootstrap_stow_checkout'
+ done
+}
+
+test_clean_install_applies_previewed_links
+test_existing_links_are_idempotent
+test_conflicts_are_backed_up_after_confirmation
+test_decline_preserves_conflicts
+test_no_stow_skips_all_stow_commands
+test_no_stow_argument_sets_skip_flag
+test_missing_stow_uses_selected_platform_installer
+test_noninteractive_input_never_applies_stow
+test_entrypoints_use_checkout_relative_paths
+
+printf 'Bootstrap Stow tests passed.\n'
\ No newline at end of file