Update Aliases Follow-up Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Eliminate the duplicate prefix-detection logic in _up_sync_aliases, add a --no-profile flag to _aliases_main so the update path doesn’t rewrite the user’s shell profile on every run, and cover the previously-untested code paths.
Architecture: Two-file edit. (1) bin/install/aliases.sh: enhance _aliases_read_existing_prefix with an alias-name fallback (moved from the soon-to-be-deleted helper in update.sh), add a --no-profile flag to _aliases_main, update _aliases_show_help. (2) bin/lib/update.sh: delete _up_aliases_read_prefix entirely, simplify _up_sync_aliases to a single install.sh aliases --no-profile invocation. Tests: replace one _up_aliases_read_prefix test, update one log-message assertion, add 5 new tests across two BATS files.
Tech Stack: Bash 5.x, BATS (Bash Automated Testing System).
Design doc: docs/plans/2026-05-10-update-aliases-followup-design.md
Branch: refactor/update-aliases-followup (already created from main, design doc committed at d6aa0db).
Single PR, single logical commit. The refactor and the test changes are tightly coupled — splitting would leave intermediate states with deleted helpers and untested new behavior.
Background
Current bin/lib/update.sh:_up_sync_aliases does:
- Check
bin/aliases.shexists → skip with INFO if not. - Call
_up_aliases_read_prefix(local helper, 22 lines) to extract the prefix from the file’s header (with alias-name fallback). - Skip with WARN if prefix is empty.
- Dry-run? Log and return.
- Run
install.sh aliases --prefix <prefix>→ which invokes_aliases_install_source_line(rewrites the user’s.bashrc/.zshrcline every update).
The duplication: _up_aliases_read_prefix reimplements _aliases_read_existing_prefix in bin/install/aliases.sh:199 with subtle differences (the update version has a robust whitespace trim and the alias-name fallback; the install version doesn’t).
After this PR:
- Check
bin/aliases.shexists → skip with INFO if not. - Dry-run? Log and return.
- Run
install.sh aliases --no-profile(no--prefix)._aliases_mainreads the existing prefix via_aliases_read_existing_prefix(now with the fallback), regenerates the file, skips the source-line install because--no-profile.
Net result: bin/lib/update.sh shrinks. bin/install/aliases.sh grows by one flag plus a fallback in the prefix reader. Profile is never touched during updates.
Task 1: Enhance _aliases_read_existing_prefix in bin/install/aliases.sh
Files:
- Modify:
bin/install/aliases.sh:199-209
Step 1: Replace the function body
Find the existing function at line 199:
_aliases_read_existing_prefix() {
if [[ -f "$ALIASES_FILE" ]]; then
local line
line="$(grep '^# Prefix:' "$ALIASES_FILE" 2>/dev/null || true)"
if [[ -n "$line" ]]; then
echo "${line#*: }"
return 0
fi
fi
echo ""
}
Replace with:
_aliases_read_existing_prefix() {
if [[ -f "$ALIASES_FILE" ]]; then
local line
line="$(grep -m1 '^# Prefix:' "$ALIASES_FILE" 2>/dev/null || true)"
if [[ -n "$line" ]]; then
# Trim leading/trailing whitespace after the colon.
line="${line#*:}"
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
echo "$line"
return 0
fi
# Fallback: extract prefix from the first 'alias <prefix>-check-health=' line.
line="$(grep -m1 '^alias [^=]*-check-health=' "$ALIASES_FILE" 2>/dev/null || true)"
if [[ -n "$line" ]]; then
line="${line#alias }"
line="${line%%=*}"
echo "${line%-check-health}"
return 0
fi
fi
echo ""
}
Two changes:
- The header trim uses bash parameter expansion to strip leading/trailing whitespace (handles multiple spaces, tabs).
- A new fallback parses the
alias <prefix>-check-health=line when the header is missing or corrupt.
Step 2: Verify syntax
bash -n bin/install/aliases.sh
Expected: exit 0.
Task 2: Add --no-profile flag to _aliases_main
Files:
- Modify:
bin/install/aliases.sh:215-300
Step 1: Add a new local var and a new flag case branch
Inside _aliases_main (line 215), add local skip_source_line=false near the other locals (line 217 area). Then add a new case in the arg-parse while loop:
--no-profile)
skip_source_line=true
i=$((i + 1))
;;
Place it next to the other flag handlers (after --help|-h, before the *) default).
Step 2: Guard the source-line install
In the setup) case (around line 297-298), change:
info "Using prefix: $prefix"
_aliases_generate "$prefix"
export ALIAS_PREFIX="$prefix"
_aliases_install_source_line
;;
to:
info "Using prefix: $prefix"
_aliases_generate "$prefix"
export ALIAS_PREFIX="$prefix"
[ "$skip_source_line" = false ] && _aliases_install_source_line
;;
Step 3: Verify syntax
bash -n bin/install/aliases.sh
Expected: exit 0.
Task 3: Update _aliases_show_help to document --no-profile
Files:
- Modify:
bin/install/aliases.sh:52-71
Step 1: Add the flag to the help text
Find the Options block:
Options:
--prefix VALUE Alias prefix (default: sm)
Example: --prefix maint => maint-check-health, ...
--remove Remove generated aliases and the source line from shell profile
--list Show currently generated aliases
--help Show this help message
Add one line after --list:
--no-profile Regenerate aliases file but skip modifying shell profile
So the Options block becomes:
Options:
--prefix VALUE Alias prefix (default: sm)
Example: --prefix maint => maint-check-health, ...
--remove Remove generated aliases and the source line from shell profile
--list Show currently generated aliases
--no-profile Regenerate aliases file but skip modifying shell profile
--help Show this help message
Step 2: Verify syntax
bash -n bin/install/aliases.sh
Task 4: Delete _up_aliases_read_prefix and simplify _up_sync_aliases
Files:
- Modify:
bin/lib/update.sh:268-321
Step 1: Read the current implementation to confirm line numbers
sed -n '265,325p' bin/lib/update.sh
You’ll see:
- Lines 268-289:
_up_aliases_read_prefix()(to delete) - Lines 290-291: a blank line and the start of
_up_sync_aliases - Lines 292-321:
_up_sync_aliases()(to simplify)
Step 2: Delete _up_aliases_read_prefix entirely
Remove lines 268-289 (the entire function and its trailing blank line). The function above (_up_sync_env close brace at 265) flows directly into _up_sync_aliases.
Step 3: Replace _up_sync_aliases body
Replace the entire _up_sync_aliases function (was lines 292-321) with:
_up_sync_aliases() {
local aliases_file="$BIN_DIR/aliases.sh"
if [ ! -f "$aliases_file" ]; then
_up_log "INFO" "Aliases not configured, skipping aliases sync"
return 0
fi
_up_log "INFO" "Regenerating aliases from install template"
if [ "$_up_dry_run" = true ]; then
_up_log "INFO" "Dry-run: would run install.sh aliases --no-profile"
return 0
fi
if ! (cd "$PROJECT_DIR" && "$BIN_DIR/install.sh" aliases --no-profile </dev/null); then
_up_log "WARN" "Alias regeneration failed; keeping existing aliases"
return 0
fi
_up_log "OK" "Aliases regenerated from install template"
return 0
}
Key changes vs. pre-PR-#143-followup:
- No
_up_aliases_read_prefixcall. - No
prefixlocal variable. - No
Could not detect alias prefixWARN path (now handled by_aliases_mainfalling back to the existing-prefix orsmdefault automatically). install.sh aliases --no-profile(no--prefix $prefixarg).- Dry-run message updated to reflect the new invocation.
Step 4: Verify syntax
bash -n bin/lib/update.sh
Expected: exit 0.
Task 5: Update existing test in test_update.bats
Two existing tests reference _up_aliases_read_prefix or the old log message. Update them.
Files:
- Modify:
bin/tests/test_update.bats:42-72
Step 1: Delete the prefix-extraction test
The test “update lib reads alias prefix from generated aliases file” (around line 42) tests _up_aliases_read_prefix. That function is gone. Delete the entire test block (the @test "update lib reads alias prefix from generated aliases file" { ... } block, including the trailing blank line if any).
Equivalent coverage lands in Task 7 below (testing _aliases_read_existing_prefix directly in test_install.bats).
Step 2: Update the dry-run test’s assertion
The test “update lib dry-run sync aliases logs regeneration step” (around line 57) asserts:
assert_output --partial "Dry-run: would run install.sh aliases --prefix sm"
Update to:
assert_output --partial "Dry-run: would run install.sh aliases --no-profile"
The fixture inside that test can stay as-is (the test setup writes a bin/aliases.sh with Prefix: sm; the new code doesn’t care about the prefix because it passes no --prefix flag). Only the assertion string changes.
Step 3: Run the modified test file
bin/tests/test_helper/bats-core/bin/bats bin/tests/test_update.bats
Expected: all tests pass. (One test was deleted, one assertion was updated; the rest are unchanged.)
Task 6: Add new tests to test_update.bats
Files:
- Modify:
bin/tests/test_update.bats(append to the end)
Step 1: Add two new tests
Append after the existing tests:
@test "_up_sync_aliases skips when aliases.sh does not exist" {
run bash -c '
source "'"$LIB_DIR/update.sh"'"
temp_bin="$(mktemp -d)"
BIN_DIR="$temp_bin"
PROJECT_DIR="$(dirname "$temp_bin")"
_up_dry_run=false
_up_json_mode=false
_up_sync_aliases
'
assert_success
assert_output --partial "Aliases not configured"
}
@test "_up_sync_aliases logs WARN and returns 0 when install.sh fails" {
run bash -c '
source "'"$LIB_DIR/update.sh"'"
temp_bin="$(mktemp -d)"
BIN_DIR="$temp_bin"
PROJECT_DIR="$(dirname "$temp_bin")"
# Provide a fake aliases.sh so the early-return is skipped.
cat > "$BIN_DIR/aliases.sh" <<EOF
# Prefix: sm
alias sm-check-health='\''cd "/tmp" && true'\''
EOF
# Fake install.sh that exits 1.
cat > "$BIN_DIR/install.sh" <<EOF
#!/usr/bin/env bash
exit 1
EOF
chmod +x "$BIN_DIR/install.sh"
_up_dry_run=false
_up_json_mode=false
_up_sync_aliases
'
assert_success
assert_output --partial "Alias regeneration failed"
}
Step 2: Run the new tests
bin/tests/test_helper/bats-core/bin/bats bin/tests/test_update.bats
Expected: all tests pass. The two new tests verify the skip-on-missing-file and regen-failure code paths.
Task 7: Add tests to test_install.bats for the prefix reader and --no-profile flag
Files:
- Modify:
bin/tests/test_install.bats(append to the end)
The existing test_install.bats is mostly syntax checks. We’ll add 5 new tests that source the aliases module and exercise its functions.
A complication: bin/install/aliases.sh calls _aliases_main "$@" at module level. Sourcing it from a test would execute the main flow with whatever args the test provides — potentially modifying the user’s actual shell profile. We need to source carefully.
Strategy: spawn a subshell, override HOME to a temp dir so any profile edits land safely, set BIN_DIR and _INSTALL_DIR so the module knows where to write, then source the module. The _aliases_main call at module level will dispatch based on the args we pass.
For the read-existing-prefix tests, we’ll source the module with --help (a no-op) to define the functions without triggering generate or remove, then call _aliases_read_existing_prefix directly.
Step 1: Add the 5 tests
Append after the existing tests:
@test "_aliases_read_existing_prefix returns prefix from # Prefix: header" {
run bash -c '
export HOME="$(mktemp -d)"
export BIN_DIR="$(mktemp -d)"
export PROJECT_DIR="$(dirname "$BIN_DIR")"
cat > "$BIN_DIR/aliases.sh" <<EOF
# Prefix: maint
alias maint-check-health='\''cd "/tmp" && true'\''
EOF
source "'"$BIN_DIR_REAL/install/aliases.sh"'" --help >/dev/null
ALIASES_FILE="$BIN_DIR/aliases.sh"
_aliases_read_existing_prefix
'
assert_success
assert_output "maint"
}
@test "_aliases_read_existing_prefix falls back to alias-name parsing when header missing" {
run bash -c '
export HOME="$(mktemp -d)"
export BIN_DIR="$(mktemp -d)"
export PROJECT_DIR="$(dirname "$BIN_DIR")"
cat > "$BIN_DIR/aliases.sh" <<EOF
# No prefix header here
alias custom-check-health='\''cd "/tmp" && true'\''
alias custom-run-check='\''cd "/tmp" && true'\''
EOF
source "'"$BIN_DIR_REAL/install/aliases.sh"'" --help >/dev/null
ALIASES_FILE="$BIN_DIR/aliases.sh"
_aliases_read_existing_prefix
'
assert_success
assert_output "custom"
}
@test "_aliases_read_existing_prefix returns empty when both methods fail" {
run bash -c '
export HOME="$(mktemp -d)"
export BIN_DIR="$(mktemp -d)"
export PROJECT_DIR="$(dirname "$BIN_DIR")"
cat > "$BIN_DIR/aliases.sh" <<EOF
# Some other file with no header and no -check-health alias
alias something-else='\''cd "/tmp" && true'\''
EOF
source "'"$BIN_DIR_REAL/install/aliases.sh"'" --help >/dev/null
ALIASES_FILE="$BIN_DIR/aliases.sh"
_aliases_read_existing_prefix
'
assert_success
assert_output ""
}
@test "install.sh aliases --no-profile regenerates aliases without modifying profile" {
run bash -c '
export HOME="$(mktemp -d)"
: > "$HOME/.bashrc"
# Use the real BIN_DIR but a clean ALIASES_FILE target.
export TEST_BIN="$(mktemp -d)"
export BIN_DIR="$TEST_BIN"
export PROJECT_DIR="$(dirname "$TEST_BIN")"
mkdir -p "$TEST_BIN/install" "$TEST_BIN/lib"
# Copy the real lib helpers + aliases module so the SCRIPT can source them.
cp -r "'"$BIN_DIR_REAL/lib"'/." "$TEST_BIN/lib/"
cp "'"$BIN_DIR_REAL/install/aliases.sh"'" "$TEST_BIN/install/aliases.sh"
# Invoke the module directly with --no-profile and a known prefix.
bash "$TEST_BIN/install/aliases.sh" --prefix sm --no-profile >/dev/null 2>&1
# Aliases file was written...
[ -f "$TEST_BIN/aliases.sh" ]
# ...but the profile was NOT touched.
[ ! -s "$HOME/.bashrc" ]
'
assert_success
}
@test "install.sh aliases --prefix without --no-profile DOES modify profile" {
run bash -c '
export HOME="$(mktemp -d)"
export SHELL=/bin/bash
: > "$HOME/.bashrc"
export TEST_BIN="$(mktemp -d)"
export BIN_DIR="$TEST_BIN"
export PROJECT_DIR="$(dirname "$TEST_BIN")"
mkdir -p "$TEST_BIN/install" "$TEST_BIN/lib"
cp -r "'"$BIN_DIR_REAL/lib"'/." "$TEST_BIN/lib/"
cp "'"$BIN_DIR_REAL/install/aliases.sh"'" "$TEST_BIN/install/aliases.sh"
bash "$TEST_BIN/install/aliases.sh" --prefix sm >/dev/null 2>&1
[ -f "$TEST_BIN/aliases.sh" ]
# Source line WAS added to the (otherwise empty) profile.
grep -qF "server-maintanence aliases" "$HOME/.bashrc"
'
assert_success
}
Note about $BIN_DIR_REAL: the inner bash subshells reset BIN_DIR to a temp dir, so they can’t reach the real bin/lib or bin/install. Capture the real BIN_DIR before the subshell:
At the top of each test method, before the run bash -c '...' line, add:
local BIN_DIR_REAL="$BIN_DIR"
export BIN_DIR_REAL
Then inside the heredoc, reference '"$BIN_DIR_REAL/lib"' etc. (already shown above).
If the heredoc-escape gymnastics get unmanageable, fall back to writing the test body to a temp script file and invoking it — or use bats’s setup_file to compute paths once. Pick whichever form actually works on your BATS version.
Step 2: Run the new tests
bin/tests/test_helper/bats-core/bin/bats bin/tests/test_install.bats
Expected: all tests pass. If the heredoc escaping is brittle, simplify the test bodies — the assertions are the load-bearing part.
Task 8: Verify, lint, manual sanity check
Step 1: Full BATS suite
for f in bin/tests/*.bats; do
echo "=== $f ==="
bin/tests/test_helper/bats-core/bin/bats "$f" || echo "FAILED: $f"
done
Expected: every *.bats file passes. The other test files (test_cli.bats, test_set_production.bats) shouldn’t be affected.
Step 2: Python suite (sanity — no Python files touched)
uv run pytest apps/ 2>&1 | tail -3
Expected: baseline test count passing, no regressions.
Step 3: Lint, format, type-check
uv run black --check apps/ 2>&1 | tail -3
uv run ruff check apps/ 2>&1 | tail -3
uv run mypy apps/ 2>&1 | tail -3
Expected: clean. (No Python files touched, but pre-commit hooks will run.)
Step 4: Manual sanity check
Run on this Mac (without modifying the local bin/aliases.sh or .zshrc):
# Verify the --no-profile flag works on the real install module.
TMP_HOME="$(mktemp -d)"
HOME="$TMP_HOME" SHELL=/bin/bash bin/install.sh aliases --prefix sm --no-profile
# Check that aliases.sh got rewritten:
ls -la bin/aliases.sh
# Check that $TMP_HOME has no .bashrc:
ls "$TMP_HOME/.bashrc" 2>&1 | head -1
# (Should say "No such file" — flag worked.)
rm -rf "$TMP_HOME"
# Verify the update path is dry-run-safe.
bin/update.sh --dry-run 2>&1 | grep -i "alias"
# Expected: "Dry-run: would run install.sh aliases --no-profile" appears.
If anything looks wrong, STOP and report.
Task 9: Commit
git add bin/install/aliases.sh bin/lib/update.sh bin/tests/test_install.bats bin/tests/test_update.bats
git commit -m "$(cat <<'EOF'
refactor(cli): consolidate alias prefix detection; add --no-profile
PR #143 (closing #142) added _up_sync_aliases for the update flow but
duplicated prefix-detection logic and rewrote the user's shell profile
on every update.
Consolidate:
- Move the alias-name fallback into _aliases_read_existing_prefix
(canonical location in bin/install/aliases.sh).
- Delete _up_aliases_read_prefix from bin/lib/update.sh.
- Add --no-profile flag to _aliases_main; skip
_aliases_install_source_line when set.
- Simplify _up_sync_aliases to a single
`install.sh aliases --no-profile` invocation. The install module
handles prefix detection automatically.
Net: bin/lib/update.sh shrinks (~22 lines deleted); bin/install/aliases.sh
gains the fallback + flag (~12 lines). User's shell profile is never
touched during updates.
Tests:
- Delete the now-obsolete _up_aliases_read_prefix test.
- Update the dry-run test's expected log message
(--prefix sm -> --no-profile).
- Add 2 tests for _up_sync_aliases skip-on-missing-file and
regen-failure paths.
- Add 5 tests for _aliases_read_existing_prefix (header, fallback,
empty) and --no-profile (profile NOT modified vs. IS modified).
Backwards compatible: existing `install.sh aliases --prefix sm`
invocations behave identically (profile is still updated). The
--no-profile flag is additive.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
Pre-commit hooks should pass (no Python files touched).
Task 10: Push and open PR
git push -u origin refactor/update-aliases-followup
gh pr create --base main --title "refactor(cli): consolidate alias prefix detection; add --no-profile" --body "$(cat <<'EOF'
## Summary
Follow-up to PR #143 (which closed #142). Three audit findings addressed:
1. **Duplicate prefix-detection logic**: `_up_aliases_read_prefix` in `bin/lib/update.sh` reimplemented `_aliases_read_existing_prefix` from `bin/install/aliases.sh` with subtle differences. Consolidated into the canonical location with the fallback intact.
2. **Shell-profile side effect on every update**: `_up_sync_aliases` called `install.sh aliases --prefix <prefix>` which invoked `_aliases_install_source_line` — rewriting `.bashrc`/`.zshrc` on every update. Added a `--no-profile` flag to suppress the source-line install.
3. **Test coverage gaps**: only 2 of 5 code paths in `_up_sync_aliases` were tested. Added BATS tests for the missing paths (skip-on-missing-file, regen-failure) and for the new `_aliases_read_existing_prefix` fallback and `--no-profile` flag.
Design doc: `docs/plans/2026-05-10-update-aliases-followup-design.md`
## Changes
- **`bin/install/aliases.sh`**: enhance `_aliases_read_existing_prefix` with alias-name fallback; add `--no-profile` flag to `_aliases_main`; update help text.
- **`bin/lib/update.sh`**: delete `_up_aliases_read_prefix` (~22 lines); simplify `_up_sync_aliases` to a single `install.sh aliases --no-profile` invocation.
- **`bin/tests/test_update.bats`**: delete now-obsolete prefix-extraction test; update one log-message assertion; add 2 new tests for the skip and regen-failure paths.
- **`bin/tests/test_install.bats`**: add 5 new tests covering prefix detection (header, fallback, empty) and `--no-profile` (profile NOT modified vs. IS modified).
## Behavior changes
- **Shell profile is no longer modified during `bin/update.sh`**. The source line is written by `bin/install.sh aliases` (interactive flow); subsequent updates just regenerate the aliases file via `--no-profile`.
- **`install.sh aliases --no-profile`** is a new public flag. Documented in `_aliases_show_help`. Useful for operators who want to regen aliases without touching their profile.
- **Existing `install.sh aliases --prefix sm`** invocations behave identically (profile is still updated). The flag is additive; no breaking change.
## Test plan
- [x] `bash -n` clean on modified files.
- [x] All BATS tests pass (`bin/tests/*.bats`).
- [x] `uv run pytest apps/` — baseline test count passes (no Python files touched).
- [x] Manual: `bin/install.sh aliases --prefix sm --no-profile` with `HOME` in a temp dir regenerates `bin/aliases.sh` and does NOT create or modify the temp `.bashrc`.
- [x] Manual: `bin/update.sh --dry-run` logs `"Dry-run: would run install.sh aliases --no-profile"`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
Return the PR URL.
Notes for the implementer
- Single PR, single commit. The refactor (delete-helper + simplify + add-flag) and the test changes are tightly coupled. Don’t split — intermediate states would have deleted helpers and untested new behavior.
bin/aliases.shis gitignored — do not commit any locally-regenerated copy.- Match existing log style.
_up_log "INFO",_up_log "OK",_up_log "WARN"inbin/lib/update.sh.info/success/warn/errorinbin/install/aliases.sh(fromlib/logging.sh). - BATS heredoc escaping is fiddly. The escape pattern
'\''insidebash -c '...'heredocs is correct but easy to get wrong. If escapes get unmanageable, write the test body to a temp script file andbash $scriptinstead. HOMEmust be a temp dir in tests that exercise profile modification. Never leaveHOMEas the user’s real home — even with idempotent writes, you don’t want your CI to touch~/.bashrc.SHELLenv var drives_aliases_detect_profile’s choice of.bashrcvs.zshrc. SettingSHELL=/bin/bashin tests makes the profile target predictable.- The
setup)case in_aliases_mainis where_aliases_generateand_aliases_install_source_lineare called. The--no-profileflag’s only effect is guarding the second call. Thesetupaction is the default;--remove,--list,--helpare unaffected. _aliases_show_helpis at line 52. The new--no-profileline goes alphabetically near the bottom of the Options block (after--list).- Don’t touch
bin/install.sh— it just forwards tobin/install/aliases.shand doesn’t need to know about the new flag.