diff --git a/README.md b/README.md index 7fb5001..cdd0417 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,12 @@ Running `./sync.sh` updates only those components: ./sync.sh --all --dry-run ``` +Machines configured before v1.2 are migrated automatically. If no state file +exists, `sync.sh` first detects configs already linked to this repository. When +none can be detected, it preserves the original behavior by syncing the legacy +Shell, WezTerm, Neovim, and AeroSpace config set. Every successful sync also +reinstalls this repository's Git hooks before migrated component state is saved. + ## 🧩 Adding software Components are auto-discovered from `components/*.sh`. A normal Homebrew formula diff --git a/lib/core.sh b/lib/core.sh index 6d3f622..cccb0a2 100755 --- a/lib/core.sh +++ b/lib/core.sh @@ -373,6 +373,58 @@ pm_sync_declared_configs() { done <<< "$configs" } +pm_paths_match() { + local first="$1" + local second="$2" + local first_dir second_dir first_path second_path + + first_dir="$(cd "$(dirname "$first")" 2>/dev/null && pwd -P)" || return 1 + second_dir="$(cd "$(dirname "$second")" 2>/dev/null && pwd -P)" || return 1 + first_path="$first_dir/$(basename "$first")" + second_path="$second_dir/$(basename "$second")" + [ "$first_path" = "$second_path" ] +} + +pm_symlink_points_to() { + local destination="$1" + local expected_source="$2" + [ -L "$destination" ] || return 1 + + local link_target + link_target="$(readlink "$destination")" || return 1 + case "$link_target" in + /*) ;; + *) link_target="$(dirname "$destination")/$link_target" ;; + esac + pm_paths_match "$link_target" "$expected_source" +} + +pm_component_has_managed_config() { + local id="$1" + local configs line source_path destination + configs="$(pm_component_field "$id" COMPONENT_CONFIGS)" + [ -n "$configs" ] || return 1 + + while IFS= read -r line; do + [ -z "$line" ] && continue + IFS='|' read -r source_path destination <<< "$line" + if pm_symlink_points_to "$HOME/$destination" "$POWER_MAC_ROOT/$source_path"; then + return 0 + fi + done <<< "$configs" + return 1 +} + +pm_detect_legacy_components() { + local id + while IFS= read -r id; do + [ -n "$id" ] || continue + if pm_component_has_managed_config "$id"; then + printf '%s\n' "$id" + fi + done < <(pm_selectable_component_ids) +} + pm_install_brew_component() { local id="$1" local kind package installed_name diff --git a/sync.sh b/sync.sh index 8c13903..911d426 100755 --- a/sync.sh +++ b/sync.sh @@ -15,6 +15,8 @@ Usage: ./sync.sh --components ID,ID,... [--dry-run] Without selection flags, sync.sh reads the components recorded by install.sh. +If no state exists, it detects configs linked by older power_mac releases and +migrates them after a successful sync. Options: --all Sync every selectable component configuration. @@ -52,7 +54,10 @@ done pm_load_components SELECTED_COMPONENTS=() -if pm_load_state; then +STATE_LOADED=false +MIGRATE_STATE=false +if pm_load_state && [ "${#PM_STATE_COMPONENTS[@]}" -gt 0 ]; then + STATE_LOADED=true POWER_MAC_TMUX_STYLE="$PM_STATE_TMUX_STYLE" fi @@ -66,10 +71,21 @@ case "$SELECTION_MODE" in pm_split_csv "$COMPONENTS_ARGUMENT" SELECTED_COMPONENTS ;; state) - if [ "${#PM_STATE_COMPONENTS[@]}" -eq 0 ]; then - pm_die "No saved component state. Run ./install.sh or use sync.sh --all/--components." + if [ "$STATE_LOADED" = true ]; then + SELECTED_COMPONENTS=("${PM_STATE_COMPONENTS[@]}") + else + while IFS= read -r id; do + [ -n "$id" ] && SELECTED_COMPONENTS+=("$id") + done < <(pm_detect_legacy_components) + + if [ "${#SELECTED_COMPONENTS[@]}" -gt 0 ]; then + pm_warn "No saved state found; detected legacy power_mac configs: $(pm_join_by , "${SELECTED_COMPONENTS[@]}")" + else + SELECTED_COMPONENTS=(shell wezterm neovim aerospace) + pm_warn "No saved state or managed symlinks found; using the legacy sync configuration" + fi + MIGRATE_STATE=true fi - SELECTED_COMPONENTS=("${PM_STATE_COMPONENTS[@]}") ;; esac @@ -93,4 +109,22 @@ if [ "${#FAILURES[@]}" -gt 0 ]; then exit 1 fi +pm_step "Repository setup" +if [ "$POWER_MAC_DRY_RUN" = true ]; then + pm_ok "[dry-run] Would install this repository's Git hooks" +elif bash "$SCRIPT_DIR/scripts/install-hooks.sh"; then + pm_ok "Git hooks installed" +else + pm_error "Configs synced, but Git hooks could not be installed" + exit 1 +fi + +if [ "$MIGRATE_STATE" = true ]; then + if pm_save_state "$POWER_MAC_TMUX_STYLE" "${SELECTED_COMPONENTS[@]}"; then + pm_ok "Saved detected components for future syncs" + else + pm_warn "Configs synced, but component state could not be saved" + fi +fi + printf '\n%sSelected configurations are in sync.%s\n\n' "$PM_GREEN" "$PM_RESET" diff --git a/tests/installer_test.sh b/tests/installer_test.sh index 51f509d..e5d721b 100755 --- a/tests/installer_test.sh +++ b/tests/installer_test.sh @@ -57,6 +57,12 @@ EOF cat > "$directory/git" <<'EOF' #!/usr/bin/env bash printf 'git %s\n' "$*" >> "${POWER_MAC_TEST_LOG:?}" +if [ "${POWER_MAC_FAIL_HOOKS:-false}" = true ] && + [ "$1" = -C ] && + [ "$3" = config ] && + [ "$4" = core.hooksPath ]; then + exit 1 +fi if [ "$1" = clone ]; then destination="${@: -1}" mkdir -p "$destination/.git" @@ -119,6 +125,7 @@ run_sync() { HOME="$home" \ PATH="$TEST_TMP/fake-bin:$PATH" \ POWER_MAC_TEST_LOG="$TEST_TMP/commands.log" \ + POWER_MAC_FAIL_HOOKS="${POWER_MAC_FAIL_HOOKS:-false}" \ "$ROOT/sync.sh" "$@" } @@ -179,12 +186,59 @@ else fail "saved-state sync touches only recorded component configs" fi +legacy_home="$TEST_TMP/home-legacy-detected" +mkdir -p "$legacy_home/.config/wezterm" "$legacy_home/.config" +ln -s "$ROOT/wezterm.lua" "$legacy_home/.config/wezterm/wezterm.lua" +ln -s "$ROOT/nvim" "$legacy_home/.config/nvim" +: > "$TEST_TMP/commands.log" +output="$(run_sync "$legacy_home")" +if assert_contains "$output" "detected legacy power_mac configs: wezterm,neovim" && + assert_file_contains "$legacy_home/.config/power_mac/state" "components=wezterm,neovim" && + assert_file_contains "$TEST_TMP/commands.log" "config core.hooksPath .githooks" && + [ ! -e "$legacy_home/.zshrc" ] && + [ ! -e "$legacy_home/.aerospace.toml" ]; then + pass "legacy migration reinstalls hooks before saving state" +else + fail "legacy migration reinstalls hooks before saving state" +fi + empty_home="$TEST_TMP/home-no-state" mkdir -p "$empty_home" -if run_sync "$empty_home" >/dev/null 2>&1; then - fail "sync without state or explicit selection is rejected" +output="$(run_sync "$empty_home")" +if assert_contains "$output" "using the legacy sync configuration" && + [ -L "$empty_home/.zshrc" ] && + [ -L "$empty_home/.config/wezterm/wezterm.lua" ] && + [ -L "$empty_home/.config/nvim" ] && + [ -L "$empty_home/.aerospace.toml" ] && + [ ! -e "$empty_home/.tmux.conf" ] && + assert_file_contains "$empty_home/.config/power_mac/state" "components=shell,wezterm,neovim,aerospace"; then + pass "sync without state falls back to the legacy config set" +else + fail "sync without state falls back to the legacy config set" +fi + +dry_migration_home="$TEST_TMP/home-legacy-dry" +mkdir -p "$dry_migration_home/.config/wezterm" +ln -s "$ROOT/wezterm.lua" "$dry_migration_home/.config/wezterm/wezterm.lua" +: > "$TEST_TMP/commands.log" +output="$(run_sync "$dry_migration_home" --dry-run)" +if assert_contains "$output" "Would install this repository's Git hooks" && + [ ! -e "$dry_migration_home/.config/power_mac/state" ] && + [ ! -s "$TEST_TMP/commands.log" ]; then + pass "legacy migration dry-run reports hooks without side effects" +else + fail "legacy migration dry-run reports hooks without side effects" +fi + +failed_hooks_home="$TEST_TMP/home-legacy-hook-failure" +mkdir -p "$failed_hooks_home/.config/wezterm" +ln -s "$ROOT/wezterm.lua" "$failed_hooks_home/.config/wezterm/wezterm.lua" +if POWER_MAC_FAIL_HOOKS=true run_sync "$failed_hooks_home" >/dev/null 2>&1; then + fail "hook failure makes legacy migration fail" +elif [ ! -e "$failed_hooks_home/.config/power_mac/state" ]; then + pass "hook failure prevents migrated state persistence" else - pass "sync without state or explicit selection is rejected" + fail "hook failure prevents migrated state persistence" fi home="$TEST_TMP/home-backup"