From 65788f83d23a64f5fcbd31e5be5e50c50df3a80f Mon Sep 17 00:00:00 2001 From: jiminu Date: Wed, 9 Sep 2026 23:44:10 +0900 Subject: [PATCH 1/2] feat: suppress already-installed output for apt, homebrew, and mise --- lib/installers.sh | 5 +++++ lib/package_managers/apt.sh | 9 +-------- lib/package_managers/homebrew.sh | 10 +--------- tests/installers_test.bash | 19 +++++++++++++++++++ tests/package_adapters_test.bash | 23 +++++++++++++++++++++++ 5 files changed, 49 insertions(+), 17 deletions(-) diff --git a/lib/installers.sh b/lib/installers.sh index 3c0fc7f..3edf8f0 100644 --- a/lib/installers.sh +++ b/lib/installers.sh @@ -141,6 +141,11 @@ install_mise_tools() { selfishell_mise_trust + if MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ + "$mise_command" install --dry-run-code -q "$@" >/dev/null 2>&1; then + return 0 + fi + if ! MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" "$mise_command" install "$@"; then if [[ "$requirement" == "optional" ]]; then cli_warn "Could not install $requirement mise tools: $*" diff --git a/lib/package_managers/apt.sh b/lib/package_managers/apt.sh index 79bb021..39631a4 100644 --- a/lib/package_managers/apt.sh +++ b/lib/package_managers/apt.sh @@ -19,7 +19,6 @@ apt_install_managed_packages() { local dry_run="$2" shift 2 local package - local installed_packages=() local missing_packages=() local available_packages=() local unavailable_packages=() @@ -46,17 +45,11 @@ apt_install_managed_packages() { # one that was removed but not purged ("rc" status: config files remain, # binaries gone) -- which would silently skip reinstalling it. Checking # the actual Status field distinguishes that from a real "ii" install. - if dpkg-query -W -f='${Status}\n' "$package" 2>/dev/null | grep -q '^install ok installed$'; then - installed_packages+=("$package") - else + if ! dpkg-query -W -f='${Status}\n' "$package" 2>/dev/null | grep -q '^install ok installed$'; then missing_packages+=("$package") fi done - if ((${#installed_packages[@]} > 0)); then - printf '%sAlready installed apt packages (%d):%s %s\n' "$SELFISHELL_COLOR_CYAN" "${#installed_packages[@]}" "$SELFISHELL_COLOR_RESET" "${installed_packages[*]}" - fi - ((${#missing_packages[@]} > 0)) || return 0 if ((SELFISHELL_APT_UPDATED == 0)); then diff --git a/lib/package_managers/homebrew.sh b/lib/package_managers/homebrew.sh index 8ca7510..d0873e1 100644 --- a/lib/package_managers/homebrew.sh +++ b/lib/package_managers/homebrew.sh @@ -49,7 +49,6 @@ homebrew_install_packages() { local manager="$2" local dry_run="$3" local package - local installed_packages=() local missing_packages=() shift 3 @@ -76,18 +75,11 @@ homebrew_install_packages() { fi for package in "$@"; do - if homebrew_package_installed "$manager" "$package"; then - installed_packages+=("$package") - else + if ! homebrew_package_installed "$manager" "$package"; then missing_packages+=("$package") fi done - if ((${#installed_packages[@]} > 0)); then - printf '%sAlready installed Homebrew %s (%d):%s %s\n' \ - "$SELFISHELL_COLOR_CYAN" "$manager" "${#installed_packages[@]}" "$SELFISHELL_COLOR_RESET" "${installed_packages[*]}" - fi - ((${#missing_packages[@]} > 0)) || return 0 if [[ "$manager" == "cask" ]]; then diff --git a/tests/installers_test.bash b/tests/installers_test.bash index efa28c1..52da0e0 100644 --- a/tests/installers_test.bash +++ b/tests/installers_test.bash @@ -10,12 +10,14 @@ source "$ROOT_DIR/lib/installers.sh" NVIM_ARGUMENTS="" MISE_ARGUMENTS="" +MISE_CALLS=() MISE_CONFIG="" GIT_ARGUMENTS="" GIT_CALLS=() NVIM_CALLS=() FAKE_NVIM_PATH="" NVIM_PLUGINS_VERIFIED=0 +MOCK_MISE_DRY_RUN_CODE_EXIT=1 nvim() { NVIM_ARGUMENTS="$*" @@ -24,12 +26,15 @@ nvim() { mise() { MISE_ARGUMENTS="$*" + MISE_CALLS+=("$*") MISE_CONFIG="$MISE_GLOBAL_CONFIG_FILE" if [[ "$1" == "which" && "$2" == "nvim" ]]; then printf '%s\n' "$FAKE_NVIM_PATH" elif [[ "$1" == "exec" && "$2" == "--" ]]; then shift 2 "$@" + elif [[ "$1" == "install" && "${2:-}" == "--dry-run-code" ]]; then + return "${MOCK_MISE_DRY_RUN_CODE_EXIT:-1}" fi } @@ -53,6 +58,7 @@ verify_neovim_plugins() { setup_installer_test() { setup_test_home export SELFISHELL_ROOT="$ROOT_DIR" + MOCK_MISE_DRY_RUN_CODE_EXIT=1 } test_installs_declared_mise_tools_with_managed_config() { @@ -66,6 +72,19 @@ test_installs_declared_mise_tools_with_managed_config() { fail "mise install did not use the Selfishell config" } +test_skips_mise_install_when_tools_are_already_installed() { + # shellcheck disable=SC2034 # Read by install_mise_tools in the sourced module. + SELFISHELL_SKIPPED_OPTIONAL_PACKAGES=() + MISE_CALLS=() + MOCK_MISE_DRY_RUN_CODE_EXIT=0 + install_mise_tools required 0 node@24.18.0 python@3.13.14 + + ((${#MISE_CALLS[@]} == 1)) || + fail "Expected only dry-run check call, got: ${MISE_CALLS[*]}" + [[ "${MISE_CALLS[0]}" == 'install --dry-run-code -q node@24.18.0 python@3.13.14' ]] || + fail "mise dry-run check was not called as expected: ${MISE_CALLS[0]}" +} + test_provisions_declared_zinit_plugins_without_loading_them() { local manifest local repository diff --git a/tests/package_adapters_test.bash b/tests/package_adapters_test.bash index e82c4a7..32d6479 100755 --- a/tests/package_adapters_test.bash +++ b/tests/package_adapters_test.bash @@ -124,6 +124,29 @@ test_apt_skips_index_update_when_packages_are_installed() { [[ -z "$MOCK_INSTALLED_PACKAGES" ]] || fail "Installed apt packages were reinstalled" } +test_apt_suppresses_already_installed_output() { + local output + reset_package_mocks + MOCK_DPKG_PACKAGES="first second" + + output="$(apt_install_managed_packages required 0 first second)" + + [[ -z "$output" ]] || fail "Installed apt packages produced output: $output" +} + +test_homebrew_suppresses_already_installed_output() { + local output + reset_package_mocks + MOCK_BREW_FORMULAE="installed" + MOCK_BREW_CASKS="font-one" + + output="$(homebrew_install_packages required formula 0 installed)" + [[ -z "$output" ]] || fail "Installed Homebrew formulae produced output: $output" + + output="$(homebrew_install_packages optional cask 0 font-one)" + [[ -z "$output" ]] || fail "Installed Homebrew casks produced output: $output" +} + test_apt_non_root_requires_sudo() { reset_package_mocks MOCK_AVAILABLE_PACKAGES="available" From cca135f281e53f6f7a3d9f870ea61087a1b8dee8 Mon Sep 17 00:00:00 2001 From: jiminu Date: Wed, 9 Sep 2026 23:55:56 +0900 Subject: [PATCH 2/2] fix(installers): place mise -q flag before install subcommand and verify fallback --- lib/installers.sh | 2 +- tests/installers_test.bash | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/installers.sh b/lib/installers.sh index 3edf8f0..bb1819b 100644 --- a/lib/installers.sh +++ b/lib/installers.sh @@ -142,7 +142,7 @@ install_mise_tools() { selfishell_mise_trust if MISE_GLOBAL_CONFIG_FILE="$SELFISHELL_ROOT/config/shared/mise.toml" \ - "$mise_command" install --dry-run-code -q "$@" >/dev/null 2>&1; then + "$mise_command" -q install --dry-run-code "$@" >/dev/null 2>&1; then return 0 fi diff --git a/tests/installers_test.bash b/tests/installers_test.bash index 52da0e0..0e79b66 100644 --- a/tests/installers_test.bash +++ b/tests/installers_test.bash @@ -33,7 +33,7 @@ mise() { elif [[ "$1" == "exec" && "$2" == "--" ]]; then shift 2 "$@" - elif [[ "$1" == "install" && "${2:-}" == "--dry-run-code" ]]; then + elif [[ "$1" == "-q" && "$2" == "install" && "${3:-}" == "--dry-run-code" ]]; then return "${MOCK_MISE_DRY_RUN_CODE_EXIT:-1}" fi } @@ -64,10 +64,16 @@ setup_installer_test() { test_installs_declared_mise_tools_with_managed_config() { # shellcheck disable=SC2034 # Read by install_mise_tools in the sourced module. SELFISHELL_SKIPPED_OPTIONAL_PACKAGES=() + MISE_CALLS=() + MOCK_MISE_DRY_RUN_CODE_EXIT=1 install_mise_tools required 0 node@24.18.0 python@3.13.14 - [[ "$MISE_ARGUMENTS" == 'install node@24.18.0 python@3.13.14' ]] || - fail "mise tools were not installed together" + ((${#MISE_CALLS[@]} == 2)) || + fail "Expected preflight check followed by install, got: ${MISE_CALLS[*]}" + [[ "${MISE_CALLS[0]}" == '-q install --dry-run-code node@24.18.0 python@3.13.14' ]] || + fail "mise preflight check was not called first: ${MISE_CALLS[0]}" + [[ "${MISE_CALLS[1]}" == 'install node@24.18.0 python@3.13.14' ]] || + fail "mise tools were not installed together: ${MISE_CALLS[1]}" [[ "$MISE_CONFIG" == "$ROOT_DIR/config/shared/mise.toml" ]] || fail "mise install did not use the Selfishell config" } @@ -81,7 +87,7 @@ test_skips_mise_install_when_tools_are_already_installed() { ((${#MISE_CALLS[@]} == 1)) || fail "Expected only dry-run check call, got: ${MISE_CALLS[*]}" - [[ "${MISE_CALLS[0]}" == 'install --dry-run-code -q node@24.18.0 python@3.13.14' ]] || + [[ "${MISE_CALLS[0]}" == '-q install --dry-run-code node@24.18.0 python@3.13.14' ]] || fail "mise dry-run check was not called as expected: ${MISE_CALLS[0]}" }