diff --git a/migrations/1787573629.sh b/migrations/1787573629.sh new file mode 100644 index 00000000..a01cc9c6 --- /dev/null +++ b/migrations/1787573629.sh @@ -0,0 +1,69 @@ +echo "Regenerate mise wrappers that still print mise's own output to stdout" + +# omarchy-mise-install gained `--quiet` on its `mise use -g` line so a wrapper +# stops printing mise's "tools: pkg@version" ahead of the tool's own output. +# That only changes wrappers written from then on, and the migration that +# installed the current ones is already marked complete, so every wrapper on +# disk keeps polluting stdout -- `claude --version` still answers with two +# lines, and a wrapper for a protocol-speaking command answers with a line its +# caller cannot parse. Rewrite them through omarchy-mise-install so the +# template stays in one place. + +# Every generated form that predates --quiet, rebuilt from the package and bin +# the file itself names. Wrappers were written by all of these over time and +# only the ones a later migration happened to regenerate moved forward, so a +# machine can still be carrying any of them. +stale_template() { + local form=$1 package=$2 bin=$3 + + case $form in + cooldown-export) + printf '#!/bin/bash\nexport MISE_MINIMUM_RELEASE_AGE=0\nmise use -g "%s" || exit 1\nexec mise x "%s" -- "%s" "$@"' "$package" "$package" "$bin" ;; + bail-on-failure) + printf '#!/bin/bash\nmise use -g "%s" || exit 1\nexec mise x "%s" -- "%s" "$@"' "$package" "$package" "$bin" ;; + mise-exec) + printf '#!/bin/bash\nmise use -g "%s"\nexec mise exec "%s" -- "%s" "$@"' "$package" "$package" "$bin" ;; + bare-exec) + printf '#!/bin/bash\nmise use -g "%s"\nexec "%s" "$@"' "$package" "$bin" ;; + esac +} + +bin_dir="$HOME/.local/bin" + +[[ -d $bin_dir ]] || exit 0 + +for wrapper in "$bin_dir"/*; do + [[ -f $wrapper && ! -L $wrapper && -r $wrapper ]] || continue + + # A generated wrapper is four short lines. ~/.local/bin also holds real + # binaries -- uv and uvx land here from the Python dev env -- so check the + # size before reading rather than pulling a 30MB executable into memory to + # discover it is not a wrapper. + (($(stat -c%s "$wrapper") <= 1024)) || continue + + contents=$(<"$wrapper") + + package=$(sed -n 's/^mise use -g "\(.*\)"\( || exit 1\)*$/\1/p' <<<"$contents") + bin=$(sed -n \ + -e 's/^exec mise x ".*" -- "\(.*\)" "\$@"$/\1/p' \ + -e 's/^exec mise exec ".*" -- "\(.*\)" "\$@"$/\1/p' \ + -e 's/^exec "\(.*\)" "\$@"$/\1/p' <<<"$contents") + + [[ -n $package && -n $bin ]] || continue + + # The whole file has to be one of those forms exactly. A wrapper someone has + # added a line to is left as it is rather than silently regenerated without + # that line, and one already carrying --quiet matches nothing here, which is + # what makes re-running this a no-op. + stale=0 + for form in cooldown-export bail-on-failure mise-exec bare-exec; do + if [[ $contents == "$(stale_template "$form" "$package" "$bin")" ]]; then + stale=1 + break + fi + done + + ((stale)) || continue + + omarchy-mise-install "$package" "${wrapper##*/}" "$bin" +done diff --git a/test/shell.d/mise-wrapper-quiet-migration-test.sh b/test/shell.d/mise-wrapper-quiet-migration-test.sh new file mode 100755 index 00000000..ff207d0f --- /dev/null +++ b/test/shell.d/mise-wrapper-quiet-migration-test.sh @@ -0,0 +1,160 @@ +#!/bin/bash + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh" + +migration="$ROOT/migrations/1787573629.sh" +test_dir=$(mktemp -d) +trap 'rm -rf "$test_dir"' EXIT + +home="$test_dir/home" +bin_dir="$home/.local/bin" +mkdir -p "$bin_dir" + +# The migration calls omarchy-mise-install to rewrite a wrapper, so the real +# one has to be reachable: this proves the template it writes today, not a +# copy of it that could drift. +run_migration() { + HOME="$home" PATH="$ROOT/bin:$PATH" bash -euo pipefail "$migration" >/dev/null +} + +write_stale_wrapper() { + local command=$1 package=$2 bin=$3 + + cat >"$bin_dir/$command" <"$bin_dir/$command" <"$bin_dir/mise-exec-era" <<'EOF' +#!/bin/bash +mise use -g "npm:some/tool" +exec mise exec "npm:some/tool" -- "tool-bin" "$@" +EOF +cat >"$bin_dir/bare-exec-era" <<'EOF' +#!/bin/bash +mise use -g "aqua:some/other" +exec "other-bin" "$@" +EOF +chmod +x "$bin_dir/mise-exec-era" "$bin_dir/bare-exec-era" + +run_migration + +grep -qF 'mise use -g --quiet "claude" || exit 1' "$bin_dir/claude" || + fail "migration adds --quiet to a stale wrapper" +pass "migration adds --quiet to a stale wrapper" + +grep -qF 'mise use -g --quiet "github:can1357/oh-my-pi" || exit 1' "$bin_dir/omp" || + fail "migration keeps a wrapper's package when the command name differs" +grep -qF 'exec mise x "github:can1357/oh-my-pi" -- "omp" "$@"' "$bin_dir/omp" || + fail "migration keeps a wrapper's bin name when the command name differs" +pass "migration preserves package and bin names" + +grep -qF 'exec mise x "npm:@kitlangton/ghui" -- "ghui" "$@"' "$bin_dir/ghui" || + fail "migration preserves a scoped npm package name" +pass "migration preserves a scoped npm package name" + +grep -qF 'mise use -g --quiet "github:someone/custom-tool" || exit 1' "$bin_dir/custom-tool" || + fail "migration rewrites a wrapper on the pre-export template" +grep -qF 'export MISE_MINIMUM_RELEASE_AGE=0' "$bin_dir/custom-tool" || + fail "migration brings a pre-export wrapper up to the current template" +pass "migration rewrites wrappers on the pre-export template" + +grep -qF 'mise use -g --quiet "npm:some/tool" || exit 1' "$bin_dir/mise-exec-era" || + fail "migration rewrites a wrapper on the mise-exec template" +grep -qF 'exec mise x "npm:some/tool" -- "tool-bin" "$@"' "$bin_dir/mise-exec-era" || + fail "migration keeps the bin name from a mise-exec wrapper" +grep -qF 'mise use -g --quiet "aqua:some/other" || exit 1' "$bin_dir/bare-exec-era" || + fail "migration rewrites a wrapper on the bare-exec template" +grep -qF 'exec mise x "aqua:some/other" -- "other-bin" "$@"' "$bin_dir/bare-exec-era" || + fail "migration keeps the bin name from a bare-exec wrapper" +pass "migration rewrites every generated form that predates --quiet" + +[[ -x $bin_dir/claude ]] || fail "migration leaves the rewritten wrapper executable" +pass "migration leaves the rewritten wrapper executable" + +# Running twice must not touch an already-quiet wrapper. +before=$(cat "$bin_dir/claude") +run_migration +[[ $(cat "$bin_dir/claude") == "$before" ]] || fail "migration is idempotent" +pass "migration is idempotent" + +# Anything the generator did not write is the user's own file. +cat >"$bin_dir/hand-written" <<'EOF' +#!/bin/bash +export MISE_MINIMUM_RELEASE_AGE=0 +mise use -g "something" || exit 1 +echo "and then something else entirely" +EOF +cat >"$bin_dir/mismatched" <<'EOF' +#!/bin/bash +export MISE_MINIMUM_RELEASE_AGE=0 +mise use -g "one-package" || exit 1 +exec mise x "another-package" -- "bin" "$@" +EOF +# A generated wrapper someone added a line to. Regenerating would drop that +# line, so the exact-match check has to leave the whole file alone. +cat >"$bin_dir/customized" <<'EOF' +#!/bin/bash +export MISE_MINIMUM_RELEASE_AGE=0 +export SOME_TOKEN=abc123 +mise use -g "customized" || exit 1 +exec mise x "customized" -- "customized" "$@" +EOF +printf '#!/bin/bash\necho hi\n' >"$bin_dir/unrelated" +# uv and uvx land in ~/.local/bin from the Python dev env. A wrapper is a few +# short lines, so a real binary must be skipped on size, never read in whole. +head -c 5000000 /dev/urandom >"$bin_dir/uv" +chmod +x "$bin_dir/uv" +ln -s "$bin_dir/claude" "$bin_dir/linked" + +hand_written_before=$(cat "$bin_dir/hand-written") +mismatched_before=$(cat "$bin_dir/mismatched") +unrelated_before=$(cat "$bin_dir/unrelated") +customized_before=$(cat "$bin_dir/customized") + +run_migration + +[[ $(cat "$bin_dir/hand-written") == "$hand_written_before" ]] || + fail "migration leaves a hand-written script that calls mise alone" +[[ $(cat "$bin_dir/mismatched") == "$mismatched_before" ]] || + fail "migration leaves a wrapper whose two lines disagree alone" +[[ $(cat "$bin_dir/unrelated") == "$unrelated_before" ]] || + fail "migration leaves an unrelated script alone" +[[ $(cat "$bin_dir/customized") == "$customized_before" ]] || + fail "migration leaves a generated wrapper a user has added a line to alone" +[[ -L $bin_dir/linked ]] || fail "migration leaves a symlink alone" +[[ $(stat -c%s "$bin_dir/uv") -eq 5000000 ]] || fail "migration leaves a native binary alone" +pass "migration only rewrites wrappers it recognizes" + +# A machine with no ~/.local/bin at all must not fail the run. +empty_home="$test_dir/empty-home" +mkdir -p "$empty_home" +HOME="$empty_home" PATH="$ROOT/bin:$PATH" bash -euo pipefail "$migration" >/dev/null || + fail "migration succeeds when ~/.local/bin is missing" +pass "migration succeeds when ~/.local/bin is missing"