From 73de806f2da9a6b65e036f383b404d8c702bd4f5 Mon Sep 17 00:00:00 2001 From: Edwin Hernandez Date: Wed, 1 Jul 2026 11:21:50 -0500 Subject: [PATCH] refactor: hoist have_any and report_failures into install-prelude The install libraries each carried a private copy of the same two helpers: a check for whether a target array holds any non-empty entry, and a warn-plus-list summary of failed items. install-prelude.sh already states its job is to hold the shared work so each library keeps only its unique logic, so both helpers move there. have_any takes the array by value (${arr[@]-}) rather than a nameref so it stays valid under the bash 3.2 that ships with macOS. report_failures takes a tag and a noun phrase so each library keeps its own wording. ai-mcp, ai-plugins, and ai-skills gain the prelude in their self-exec guard, and their bats harnesses source it alongside log.sh, matching the pattern graphify and vscode already used. Behavior is unchanged; the warning text and exit codes are identical. Fixes DOT-46 --- .../lib/common/install-prelude.sh | 29 ++++++++++++++++ home/.chezmoitemplates/lib/install/ai-mcp.sh | 31 +++-------------- .../lib/install/ai-plugins.sh | 33 ++++--------------- .../lib/install/ai-skills.sh | 31 +++-------------- .../lib/install/graphify-skills.sh | 27 ++------------- .../.chezmoitemplates/lib/install/uv-tools.sh | 3 +- home/.chezmoitemplates/lib/install/vscode.sh | 3 +- tests/unit/lib/install/ai-mcp.bats | 3 +- tests/unit/lib/install/ai-plugins.bats | 3 +- tests/unit/lib/install/ai-skills.bats | 3 +- tests/unit/lib/install/uv-tools.bats | 7 ++-- 11 files changed, 59 insertions(+), 114 deletions(-) diff --git a/home/.chezmoitemplates/lib/common/install-prelude.sh b/home/.chezmoitemplates/lib/common/install-prelude.sh index a51f025..dc5b60e 100644 --- a/home/.chezmoitemplates/lib/common/install-prelude.sh +++ b/home/.chezmoitemplates/lib/common/install-prelude.sh @@ -32,3 +32,32 @@ function require_command() { return 1 } } + +# +# @description True when any argument is a non-empty string. Pass an install +# array expanded with the ${arr[@]-} form so an unset array stays safe. +# @arg $@ Candidate entries. +# @exitcode 0 At least one non-empty entry is present. +# @exitcode 1 Every entry is empty or absent. +# +function have_any() { + local item + for item in "$@"; do + [[ -n "${item}" ]] && return 0 + done + return 1 +} + +# +# @description Warn with a count summary, then list the failed items on stderr. +# Keeps per-library wording by taking the tag and a noun phrase from the caller. +# @arg $1 Log tag, for example "uv". +# @arg $2 Noun phrase, for example "tool(s) failed to install". +# @arg $@ Remaining args: the names of the items that failed. +# +function report_failures() { + local tag="$1" summary="$2" + shift 2 + log_warn "[${tag}] ${#} ${summary}:" + printf ' - %s\n' "$@" >&2 +} diff --git a/home/.chezmoitemplates/lib/install/ai-mcp.sh b/home/.chezmoitemplates/lib/install/ai-mcp.sh index f605fd9..2bf03a8 100644 --- a/home/.chezmoitemplates/lib/install/ai-mcp.sh +++ b/home/.chezmoitemplates/lib/install/ai-mcp.sh @@ -10,36 +10,13 @@ set -Eeuo pipefail -# -# @description True when AI_MCP holds at least one non-empty entry. -# @exitcode 0 A server is present. -# @exitcode 1 No servers. -# -function _ai_mcp_have_any() { - local entry - for entry in "${AI_MCP[@]-}"; do - [[ -z "${entry}" ]] && continue - return 0 - done - return 1 -} - -# -# @description Warn about the MCP servers that failed to register. -# @arg $@ string Names of the servers that failed. -# -function _ai_mcp_report_failures() { - log_warn "[ai-mcp] ${#} server(s) failed to register:" - printf ' - %s\n' "$@" >&2 -} - # # @description Register each MCP server on Claude Code, skipping existing ones. # @exitcode 0 Registered, or nothing to do. # @exitcode 1 The claude CLI is missing. # function ai_mcp_install_main() { - if ! _ai_mcp_have_any; then + if ! have_any "${AI_MCP[@]-}"; then log_info "[ai-mcp] No MCP servers to register." return 0 fi @@ -69,7 +46,7 @@ function ai_mcp_install_main() { done if ((${#failed[@]} > 0)); then - _ai_mcp_report_failures "${failed[@]}" + report_failures ai-mcp "server(s) failed to register" "${failed[@]}" fi log_info "[ai-mcp] MCP servers registered." @@ -83,9 +60,11 @@ function main() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - # When run directly, pull in the shared log library that chezmoi otherwise + # When run directly, pull in the shared libraries that chezmoi otherwise # concatenates ahead of this file. # shellcheck source=/dev/null command -v log_info >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../common/log.sh" + # shellcheck source=/dev/null + command -v have_any >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../common/install-prelude.sh" main fi diff --git a/home/.chezmoitemplates/lib/install/ai-plugins.sh b/home/.chezmoitemplates/lib/install/ai-plugins.sh index 852ee46..d40f9b1 100644 --- a/home/.chezmoitemplates/lib/install/ai-plugins.sh +++ b/home/.chezmoitemplates/lib/install/ai-plugins.sh @@ -10,29 +10,6 @@ set -Eeuo pipefail -# -# @description True when AI_PLUGINS holds at least one non-empty entry. -# @exitcode 0 A plugin is present. -# @exitcode 1 No plugins. -# -function _ai_plugins_have_any() { - local entry - for entry in "${AI_PLUGINS[@]-}"; do - [[ -z "${entry}" ]] && continue - return 0 - done - return 1 -} - -# -# @description Warn about the plugins that failed to install. -# @arg $@ string Names of the plugins that failed. -# -function _ai_plugins_report_failures() { - log_warn "[ai-plugins] ${#} plugin(s) failed to install:" - printf ' - %s\n' "$@" >&2 -} - # # @description Add one plugin's marketplace and install it into Claude Code. # @arg $1 string Plugin name. @@ -76,7 +53,7 @@ function _ai_plugins_for_claude_code() { done if ((${#failed[@]} > 0)); then - _ai_plugins_report_failures "${failed[@]}" + report_failures ai-plugins "plugin(s) failed to install" "${failed[@]}" fi } @@ -101,7 +78,7 @@ function _ai_plugins_for_copilot() { done if ((${#failed[@]} > 0)); then - _ai_plugins_report_failures "${failed[@]}" + report_failures ai-plugins "plugin(s) failed to install" "${failed[@]}" fi } @@ -126,7 +103,7 @@ function _ai_plugins_for_target() { # @exitcode 1 A required CLI is missing for a requested target. # function ai_plugins_install_main() { - if ! _ai_plugins_have_any; then + if ! have_any "${AI_PLUGINS[@]-}"; then log_info "[ai-plugins] No plugins to install." return 0 fi @@ -158,9 +135,11 @@ function main() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - # When run directly, pull in the shared log library that chezmoi otherwise + # When run directly, pull in the shared libraries that chezmoi otherwise # concatenates ahead of this file. # shellcheck source=/dev/null command -v log_info >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../common/log.sh" + # shellcheck source=/dev/null + command -v have_any >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../common/install-prelude.sh" main fi diff --git a/home/.chezmoitemplates/lib/install/ai-skills.sh b/home/.chezmoitemplates/lib/install/ai-skills.sh index 7c37b45..90ae768 100644 --- a/home/.chezmoitemplates/lib/install/ai-skills.sh +++ b/home/.chezmoitemplates/lib/install/ai-skills.sh @@ -9,20 +9,6 @@ set -Eeuo pipefail -# -# @description True when AI_SKILLS holds at least one non-empty entry. -# @exitcode 0 A skill is present. -# @exitcode 1 No skills. -# -function _ai_skills_have_any() { - local entry - for entry in "${AI_SKILLS[@]-}"; do - [[ -z "${entry}" ]] && continue - return 0 - done - return 1 -} - # # @description Resolve a skill's install source, skipping a missing local skill. # @arg $1 string Source token: a remote repo, or "local". @@ -49,22 +35,13 @@ function _ai_skills_source() { return 1 } -# -# @description Warn about the skills that failed to install. -# @arg $@ string Names of the skills that failed. -# -function _ai_skills_report_failures() { - log_warn "[ai-skills] ${#} skill(s) failed to install:" - printf ' - %s\n' "$@" >&2 -} - # # @description Install each selected skill for the active agent targets. # @exitcode 0 Skills installed, or nothing to do. # @exitcode 1 npx is not available. # function ai_skills_install_main() { - if ! _ai_skills_have_any; then + if ! have_any "${AI_SKILLS[@]-}"; then log_info "[ai-skills] No skills to install." return 0 fi @@ -100,7 +77,7 @@ function ai_skills_install_main() { done if ((${#failed[@]} > 0)); then - _ai_skills_report_failures "${failed[@]}" + report_failures ai-skills "skill(s) failed to install" "${failed[@]}" fi log_info "[ai-skills] Cross-agent skills installed." @@ -114,9 +91,11 @@ function main() { } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - # When run directly, pull in the shared log library that chezmoi otherwise + # When run directly, pull in the shared libraries that chezmoi otherwise # concatenates ahead of this file. # shellcheck source=/dev/null command -v log_info >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../common/log.sh" + # shellcheck source=/dev/null + command -v have_any >/dev/null 2>&1 || source "$(dirname "${BASH_SOURCE[0]}")/../common/install-prelude.sh" main fi diff --git a/home/.chezmoitemplates/lib/install/graphify-skills.sh b/home/.chezmoitemplates/lib/install/graphify-skills.sh index 35f5fe2..0e133bb 100644 --- a/home/.chezmoitemplates/lib/install/graphify-skills.sh +++ b/home/.chezmoitemplates/lib/install/graphify-skills.sh @@ -8,36 +8,13 @@ set -Eeuo pipefail -# -# @description True when GRAPHIFY_PLATFORMS holds at least one non-empty entry. -# @exitcode 0 A platform is present. -# @exitcode 1 No platforms. -# -function _graphify_has_platform() { - local platform - for platform in "${GRAPHIFY_PLATFORMS[@]-}"; do - [[ -z "${platform}" ]] && continue - return 0 - done - return 1 -} - -# -# @description Warn about the platforms that failed to install. -# @arg $@ string Names of the platforms that failed. -# -function _graphify_report_failures() { - log_warn "[graphify] ${#} platform(s) failed to install:" - printf ' - %s\n' "$@" >&2 -} - # # @description Install Graphify skills for each selected platform. # @exitcode 0 Installed, or nothing to do. # @exitcode 1 The graphify CLI is missing. # function graphify_skills_install_main() { - if ! _graphify_has_platform; then + if ! have_any "${GRAPHIFY_PLATFORMS[@]-}"; then log_info "[graphify] No Graphify platforms to install." return 0 fi @@ -54,7 +31,7 @@ function graphify_skills_install_main() { done if ((${#failed[@]} > 0)); then - _graphify_report_failures "${failed[@]}" + report_failures graphify "platform(s) failed to install" "${failed[@]}" fi log_info "[graphify] Graphify agent skills installed." diff --git a/home/.chezmoitemplates/lib/install/uv-tools.sh b/home/.chezmoitemplates/lib/install/uv-tools.sh index 3625a67..c160d06 100644 --- a/home/.chezmoitemplates/lib/install/uv-tools.sh +++ b/home/.chezmoitemplates/lib/install/uv-tools.sh @@ -25,8 +25,7 @@ function uv_tools_install_main() { done if ((${#failed[@]} > 0)); then - log_warn "[uv] ${#failed[@]} tool(s) failed to install:" - printf ' - %s\n' "${failed[@]}" >&2 + report_failures uv "tool(s) failed to install" "${failed[@]}" fi log_info "[uv] uv tools installed." diff --git a/home/.chezmoitemplates/lib/install/vscode.sh b/home/.chezmoitemplates/lib/install/vscode.sh index e290bf6..3254d65 100644 --- a/home/.chezmoitemplates/lib/install/vscode.sh +++ b/home/.chezmoitemplates/lib/install/vscode.sh @@ -27,8 +27,7 @@ function vscode_install_extensions_main() { done if ((${#failed[@]} > 0)); then - log_warn "[vscode] ${#failed[@]} extension(s) failed to install:" - printf ' - %s\n' "${failed[@]}" >&2 + report_failures vscode "extension(s) failed to install" "${failed[@]}" fi log_info "[vscode] VS Code extensions installed." diff --git a/tests/unit/lib/install/ai-mcp.bats b/tests/unit/lib/install/ai-mcp.bats index c505acc..c46a597 100644 --- a/tests/unit/lib/install/ai-mcp.bats +++ b/tests/unit/lib/install/ai-mcp.bats @@ -5,6 +5,7 @@ load '../../../test_helpers/load.bash' LOG_LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/log.sh" +PRELUDE="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/install-prelude.sh" LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/install/ai-mcp.sh" setup() { @@ -24,7 +25,7 @@ CLAUDE } _run_main() { - run bash -c "source '$LOG_LIB' && source '$LIB' && $1 && main" + run bash -c "source '$LOG_LIB' && source '$PRELUDE' && source '$LIB' && $1 && main" } @test "main: registers each server with user scope and transport" { diff --git a/tests/unit/lib/install/ai-plugins.bats b/tests/unit/lib/install/ai-plugins.bats index eff9a3b..ef4896f 100644 --- a/tests/unit/lib/install/ai-plugins.bats +++ b/tests/unit/lib/install/ai-plugins.bats @@ -5,6 +5,7 @@ load '../../../test_helpers/load.bash' LOG_LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/log.sh" +PRELUDE="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/install-prelude.sh" LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/install/ai-plugins.sh" setup() { @@ -30,7 +31,7 @@ NPX } _run_main() { - run bash -c "source '$LOG_LIB' && source '$LIB' && $1 && main" + run bash -c "source '$LOG_LIB' && source '$PRELUDE' && source '$LIB' && $1 && main" } @test "main: claude-code target adds marketplace and installs each plugin" { diff --git a/tests/unit/lib/install/ai-skills.bats b/tests/unit/lib/install/ai-skills.bats index ac25740..2fc4682 100644 --- a/tests/unit/lib/install/ai-skills.bats +++ b/tests/unit/lib/install/ai-skills.bats @@ -5,6 +5,7 @@ load '../../../test_helpers/load.bash' LOG_LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/log.sh" +PRELUDE="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/install-prelude.sh" LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/install/ai-skills.sh" setup() { @@ -24,7 +25,7 @@ NPX } _run_main() { - run bash -c "source '$LOG_LIB' && source '$LIB' && $1 && main" + run bash -c "source '$LOG_LIB' && source '$PRELUDE' && source '$LIB' && $1 && main" } @test "main: installs each skill for a single target" { diff --git a/tests/unit/lib/install/uv-tools.bats b/tests/unit/lib/install/uv-tools.bats index a03ec7b..3265b3c 100644 --- a/tests/unit/lib/install/uv-tools.bats +++ b/tests/unit/lib/install/uv-tools.bats @@ -5,6 +5,7 @@ load '../../../test_helpers/load.bash' LOG_LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/log.sh" +PRELUDE="$DOTFILES_ROOT/home/.chezmoitemplates/lib/common/install-prelude.sh" LIB="$DOTFILES_ROOT/home/.chezmoitemplates/lib/install/uv-tools.sh" setup() { @@ -23,7 +24,7 @@ UV } @test "uv_tools_install_main: installs each tool with upgrade flag" { - run bash -c "source '$LOG_LIB' && source '$LIB' && UV_TOOLS=('graphifyy' 'tavily-cli') && uv_tools_install_main" + run bash -c "source '$LOG_LIB' && source '$PRELUDE' && source '$LIB' && UV_TOOLS=('graphifyy' 'tavily-cli') && uv_tools_install_main" assert_success assert_line "[uv] Installing uv tools..." @@ -32,7 +33,7 @@ UV } @test "uv_tools_install_main: skips empty tool entries" { - run bash -c "source '$LOG_LIB' && source '$LIB' && UV_TOOLS=('graphifyy' '' 'tavily-cli') && uv_tools_install_main" + run bash -c "source '$LOG_LIB' && source '$PRELUDE' && source '$LIB' && UV_TOOLS=('graphifyy' '' 'tavily-cli') && uv_tools_install_main" assert_success [ "$(<"$UV_ARGS_FILE")" = $'tool install --upgrade graphifyy\ntool install --upgrade tavily-cli' ] @@ -41,7 +42,7 @@ UV @test "uv_tools_install_main: warns but succeeds when a tool fails" { export UV_FAIL_TOOL='tavily-cli' - run bash -c "source '$LOG_LIB' && source '$LIB' && UV_TOOLS=('graphifyy' 'tavily-cli') && uv_tools_install_main" + run bash -c "source '$LOG_LIB' && source '$PRELUDE' && source '$LIB' && UV_TOOLS=('graphifyy' 'tavily-cli') && uv_tools_install_main" assert_success assert_line "[uv] Installing uv tools..."