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 Arch +- Manjaro Manjaro +- Ubuntu Ubuntu +- macOS macOS ## Window Managers @@ -295,11 +311,5 @@ scripts directory. ## WIP -- ![Gentoo\_icon][gentoo_icon] Gentoo +- 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