From f611cd2a59245400a8f460f183f26098a903b1c5 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 27 Jun 2026 02:22:11 +0000 Subject: [PATCH] feat: polish bashrc stack helpers --- TODO.md | 1 - profile/bashrc.d/00-core.bash | 26 ++++++++++++++-- profile/bashrc.d/10-tmux.bash | 2 +- profile/bashrc.d/20-git.bash | 11 +++++++ profile/bashrc.d/30-ai.bash | 58 +++++++++++++++++++++++++++++------ tests/bashrc-smoke.sh | 44 ++++++++++++++++++++++++++ 6 files changed, 128 insertions(+), 14 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 208af2b..0000000 --- a/TODO.md +++ /dev/null @@ -1 +0,0 @@ -- When genCommitMsg fails because of a rate limit, it should bail out instead of using the error as the commit message diff --git a/profile/bashrc.d/00-core.bash b/profile/bashrc.d/00-core.bash index 9019ffc..7a8284a 100644 --- a/profile/bashrc.d/00-core.bash +++ b/profile/bashrc.d/00-core.bash @@ -57,9 +57,29 @@ alias cd..='cd ..' alias ll='ls -al --color=auto' function add_alias { + if [ $# -ne 2 ]; then + echo "usage: add_alias " >&2 + return 1 + fi + # shellcheck disable=SC2139 alias "$1"="$2" - complete -F _complete_alias "$1" + if declare -F _complete_alias >/dev/null 2>&1; then + complete -F _complete_alias "$1" + fi +} + +jprofile_path_prepend() { + if [ $# -ne 1 ] || [ -z "$1" ]; then + echo "usage: jprofile_path_prepend " >&2 + return 1 + fi + [ -d "$1" ] || return 0 + + case ":$PATH:" in + *":$1:"*) ;; + *) export PATH="$1:$PATH" ;; + esac } # --- Prompt --- @@ -94,7 +114,7 @@ fi # --- fzf --- if [ -f /usr/local/etc/.fzf.bash ]; then - export PATH="/usr/local/etc/fzf/bin/:$PATH" + jprofile_path_prepend /usr/local/etc/fzf/bin if [ -z "$__JPROFILE_RELOADING_BASHRC" ]; then # shellcheck source=/dev/null source /usr/local/etc/.fzf.bash @@ -115,4 +135,4 @@ if [ -f /usr/local/etc/.fzf.bash ]; then fi # --- PATH --- -export PATH="/opt/nvim-linux64/bin:$PATH" +jprofile_path_prepend /opt/nvim-linux64/bin diff --git a/profile/bashrc.d/10-tmux.bash b/profile/bashrc.d/10-tmux.bash index cda5b41..2f3d72e 100644 --- a/profile/bashrc.d/10-tmux.bash +++ b/profile/bashrc.d/10-tmux.bash @@ -58,7 +58,7 @@ alias fts=fix-tmux-ssh # --- Tmuxifier init --- if [ -d /usr/local/etc/.tmuxifier ]; then - export PATH="/usr/local/etc/.tmuxifier/bin:$PATH" + jprofile_path_prepend /usr/local/etc/.tmuxifier/bin export TMUXIFIER_LAYOUT_PATH=/usr/local/etc/tmuxifiers/ if [ -z "$__JPROFILE_RELOADING_BASHRC" ]; then eval "$(tmuxifier init -)" diff --git a/profile/bashrc.d/20-git.bash b/profile/bashrc.d/20-git.bash index 0371bd6..8e12880 100644 --- a/profile/bashrc.d/20-git.bash +++ b/profile/bashrc.d/20-git.bash @@ -30,10 +30,21 @@ add_alias sbrc 'source ~/.bashrc' unalias gch 2>/dev/null gch() { if [ $# -eq 0 ]; then + if ! command -v fzf >/dev/null 2>&1; then + echo "gch: fzf is required when no branch argument is provided" >&2 + echo "usage: gch " >&2 + return 1 + fi + local local_branches remote_branches selected local_branches=$(git branch --format='%(refname:short)' 2>/dev/null) remote_branches=$(git branch --remote --format='%(refname:short)' 2>/dev/null | grep -v '/HEAD$') selected=$(printf '%s\n%s\n' "$local_branches" "$remote_branches" | awk '!seen[$0]++' | fzf --height=40% --reverse --prompt="Checkout branch> ") + local fzf_status=$? + if [ $fzf_status -ne 0 ]; then + [ $fzf_status -eq 130 ] && return 0 + return $fzf_status + fi [ -z "$selected" ] && return 0 if [[ "$selected" == */* ]]; then local branch_name="${selected#*/}" diff --git a/profile/bashrc.d/30-ai.bash b/profile/bashrc.d/30-ai.bash index 80bbf97..757d8f4 100644 --- a/profile/bashrc.d/30-ai.bash +++ b/profile/bashrc.d/30-ai.bash @@ -5,15 +5,26 @@ genCommitMsg() { local model="$1" + if [ -z "$model" ]; then + echo "genCommitMsg: missing model" >&2 + return 1 + fi + local repo_root repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || { echo "Not a git repository" >&2 return 1 } + if git diff --cached --quiet --exit-code; then + echo "genCommitMsg: no staged changes" >&2 + return 1 + fi + local tmp="$repo_root/.gen_commit_msg.$$.diff" git diff --staged >"$tmp" + local pi_run if [ -f "$repo_root/profile/pi-unleashed-safely.sh" ]; then pi_run="$repo_root/profile/pi-unleashed-safely.sh --dev" else @@ -31,25 +42,54 @@ genCommitMsg() { --model "$model" \ --thinking off \ "@$tmp" \ - "Write a one-line commit message for the currently staged changes following the Conventional Commits standard. Output only the commit message, no backticks, no formatting, just the text." 2>/dev/null) || exit_status=$? + "Write a one-line commit message for the currently staged changes following the Conventional Commits standard. Output only the commit message, no backticks, no formatting, just the text." 2>&1) || exit_status=$? rm -f "$tmp" if [ $exit_status -ne 0 ]; then echo "genCommitMsg: failed to generate commit message (exit code $exit_status)" >&2 + printf '%s\n' "$output" >&2 return $exit_status fi - echo "$output" | tail -n 1 + local msg + msg="$(printf '%s\n' "$output" | sed '/^[[:space:]]*$/d' | tail -n 1 | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [ -z "$msg" ]; then + echo "genCommitMsg: model returned an empty commit message" >&2 + return 1 + fi + if printf '%s' "$msg" | grep -Eiq '(^|[^a-z])(error|exception|traceback|rate limit|429|failed|unauthorized|forbidden)([^a-z]|$)'; then + echo "genCommitMsg: refusing suspicious generated commit message: $msg" >&2 + return 1 + fi + + echo "$msg" return 0 } -# shellcheck disable=SC2016 -add_alias gcoto-openai 'git commit -m "$(genCommitMsg openai-codex/gpt-5.4-mini)"' -# shellcheck disable=SC2016 -add_alias gcoto-deepseek 'git commit -m "$(genCommitMsg deepseek/deepseek-v4-flash)"' -# shellcheck disable=SC2016 -add_alias gcoto-free 'git commit -m "$(genCommitMsg opencode/mimo-v2.5-free)"' +gcoto-commit-with-model() { + local model="$1" + if [ -z "$model" ]; then + echo "usage: gcoto-commit-with-model " >&2 + return 1 + fi + + local msg + msg="$(genCommitMsg "$model")" || return 1 + git commit -m "$msg" +} + +gcoto-openai() { + gcoto-commit-with-model openai-codex/gpt-5.4-mini +} + +gcoto-deepseek() { + gcoto-commit-with-model deepseek/deepseek-v4-flash +} + +gcoto-free() { + gcoto-commit-with-model opencode/mimo-v2.5-free +} # gcoto-model: model selection helper (cached per session via _GCOTO_MODEL) # Usage: gcoto-model [openai|deepseek|free|current|unset] @@ -104,7 +144,7 @@ function gcoto { echo "gcoto: no model selected" >&2 return 1 fi - git commit -m "$(genCommitMsg "$_GCOTO_MODEL")" + gcoto-commit-with-model "$_GCOTO_MODEL" } # --- gacp (add-commit-push, depends on gcoto and gpush/gaireview from 20-git.bash) --- diff --git a/tests/bashrc-smoke.sh b/tests/bashrc-smoke.sh index ab51a2a..1e39619 100755 --- a/tests/bashrc-smoke.sh +++ b/tests/bashrc-smoke.sh @@ -83,7 +83,12 @@ _result=$(_run_interactive "$FIXTURES_DIR" ' declare -F gcoto >/dev/null && echo "__FN_gcoto__" declare -F gcoto-model >/dev/null && echo "__FN_gcoto_model__" declare -F gacp >/dev/null && echo "__FN_gacp__" + declare -F gcoto-commit-with-model >/dev/null && echo "__FN_gcoto_commit_with_model__" + declare -F gcoto-openai >/dev/null && echo "__FN_gcoto_openai__" + declare -F gcoto-deepseek >/dev/null && echo "__FN_gcoto_deepseek__" + declare -F gcoto-free >/dev/null && echo "__FN_gcoto_free__" declare -F add_alias >/dev/null && echo "__FN_add_alias__" + declare -F jprofile_path_prepend >/dev/null && echo "__FN_jprofile_path_prepend__" # Check key aliases alias gpush >/dev/null 2>&1 && echo "__AL_gpush__" @@ -111,7 +116,12 @@ assert_match "gtagpush exists" "__FN_gtagpush__" "$_result" assert_match "gcoto exists" "__FN_gcoto__" "$_result" assert_match "gcoto-model exists" "__FN_gcoto_model__" "$_result" assert_match "gacp exists" "__FN_gacp__" "$_result" +assert_match "gcoto-commit-with-model exists" "__FN_gcoto_commit_with_model__" "$_result" +assert_match "gcoto-openai exists" "__FN_gcoto_openai__" "$_result" +assert_match "gcoto-deepseek exists" "__FN_gcoto_deepseek__" "$_result" +assert_match "gcoto-free exists" "__FN_gcoto_free__" "$_result" assert_match "add_alias exists" "__FN_add_alias__" "$_result" +assert_match "jprofile_path_prepend exists" "__FN_jprofile_path_prepend__" "$_result" assert_match "gpush alias" "__AL_gpush__" "$_result" assert_match "gs alias" "__AL_gs__" "$_result" @@ -140,9 +150,16 @@ _result=$(_run_interactive "" ' count=$(printf "%s" "$PROMPT_COMMAND" | grep -o "jprofile_prompt_hook" | wc -l) echo "HOOK_COUNT=$count" + + mkdir -p "$HOME/bin" + jprofile_path_prepend "$HOME/bin" + jprofile_path_prepend "$HOME/bin" + path_count=$(printf "%s" "$PATH" | tr : "\n" | grep -c "^$HOME/bin$") + echo "PATH_COUNT=$path_count" ') assert_match "single hook after re-source" "HOOK_COUNT=1" "$_result" +assert_match "path prepend is idempotent" "PATH_COUNT=1" "$_result" # --------------------------------------------------------------------------- # Scenario 4 - Helper command sanity @@ -201,6 +218,33 @@ _result=$(_run_interactive "" ' ') assert_match "gom outside git repo returns error" "GOM_NO_GIT_OK" "$_result" +# Test gch picker propagates fzf failures in non-interactive contexts +_result=$(_run_interactive "" ' + echo "source '"$LOADER"'" > "$HOME/.bashrc" + source "$HOME/.bashrc" + if gch 2>&1; then + echo "GCH_PICKER_FAIL" + else + echo "GCH_PICKER_OK" + fi +') +assert_match "gch picker failure returns error" "GCH_PICKER_OK" "$_result" + +# Test genCommitMsg refuses to run without staged changes before invoking an agent +_result=$(_run_interactive "" ' + echo "source '"$LOADER"'" > "$HOME/.bashrc" + source "$HOME/.bashrc" + mkdir "$HOME/repo" + cd "$HOME/repo" || exit 1 + git init >/dev/null 2>&1 + if genCommitMsg openai-codex/gpt-5.4-mini 2>&1; then + echo "GEN_NO_STAGED_FAIL" + else + echo "GEN_NO_STAGED_OK" + fi +') +assert_match "genCommitMsg without staged changes returns error" "GEN_NO_STAGED_OK" "$_result" + # Test klogs_deploy usage _result=$(_run_interactive "$FIXTURES_DIR" ' echo "source '"$LOADER"'" > "$HOME/.bashrc"