aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/bin
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-19 20:40:51 -0500
committerCraig Jennings <c@cjennings.net>2026-07-19 20:40:51 -0500
commit113e8d868fdd95b269c0cc2ce4ec265aa76b539c (patch)
tree774b1e68cc26a7f0997c68bef3f115a686eb229b /claude-templates/bin
parentd49be0984bc616d584b02abbda1a91d4d46c3c80 (diff)
downloadrulesets-113e8d868fdd95b269c0cc2ce4ec265aa76b539c.tar.gz
rulesets-113e8d868fdd95b269c0cc2ce4ec265aa76b539c.zip
test(ai-launcher): net current behavior and make bin/ai source-testable
Brings the session launcher under characterization tests ahead of the hardening refactor, so the pure-core extractions and footgun fixes land against a green net. The tmux/git-coupled functions can't be reached by running the launcher black-box, so I wrapped dispatch in main() behind a run-vs-sourced guard: run directly it dispatches as before, sourced it exposes the functions to the tests. The net pins current behavior (record-not-spec) with a Normal/Boundary/Error set per unit: git_status_indicator across no-upstream/ahead/behind/dirty/clean, candidate discovery and filtering, selection parsing, and functional tests over create_window, find_window_id, and sort_windows ordering against a throwaway tmux server. Every tmux call runs on a private socket so nothing touches the live ai session, and the throwaway repos disable git auto-maintenance to dodge the teardown race that flaked rename-ai. Also cleared the two pre-existing shellcheck warnings on the touched paths: quoted the "@{u}" revspecs (SC1083) and marked the intentionally-literal display tilde (SC2088). Both are behavior-neutral, and the existing runtime tests confirm the launch path is unchanged.
Diffstat (limited to 'claude-templates/bin')
-rwxr-xr-xclaude-templates/bin/ai125
1 files changed, 69 insertions, 56 deletions
diff --git a/claude-templates/bin/ai b/claude-templates/bin/ai
index cf17875..7ce4337 100755
--- a/claude-templates/bin/ai
+++ b/claude-templates/bin/ai
@@ -138,6 +138,9 @@ create_window() {
# Add a directory to candidates only if it's a Claude-template project.
maybe_add_candidate() {
local dir="$1"
+ # The "~/" is a deliberate literal display prefix, re-expanded downstream via
+ # ${c/#\~/$HOME}; it must not expand here, so SC2088 doesn't apply.
+ # shellcheck disable=SC2088
[ -f "$dir/.ai/protocols.org" ] && candidates+=("~/${dir#"$HOME"/}")
}
@@ -179,7 +182,7 @@ git_status_indicator() {
local dir="$1" upstream ahead=0 behind=0 parts=()
[ -d "$dir/.git" ] || return 0
- upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)
+ upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null || true)
if [ -n "$upstream" ]; then
ahead=$(git -C "$dir" rev-list --count "$upstream..HEAD" 2>/dev/null || echo 0)
behind=$(git -C "$dir" rev-list --count "HEAD..$upstream" 2>/dev/null || echo 0)
@@ -225,7 +228,7 @@ auto_pull_if_clean() {
return 0
fi
- upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)
+ upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null || true)
[ -z "$upstream" ] && return 0
ahead=$(git -C "$dir" rev-list --count "$upstream..HEAD" 2>/dev/null || echo 0)
@@ -312,7 +315,7 @@ prep_git_single() {
fi
[ "$fetch_stale" -eq 1 ] && git -C "$dir" fetch --quiet 2>/dev/null || true
- upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null || true)
+ upstream=$(git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>/dev/null || true)
[ -z "$upstream" ] && return 0
ahead=$(git -C "$dir" rev-list --count "$upstream..HEAD" 2>/dev/null || echo 0)
@@ -478,63 +481,73 @@ print_launch_mode() {
# ---------- dispatch ----------
-print_launch=""
-runtime_explicit="${AI_RUNTIME:+1}"
-while [ $# -gt 0 ]; do
- case "$1" in
- -h|--help)
- usage
- ;;
- --runtime)
- [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex, local" >&2; exit 2; }
- RUNTIME="$2"
- runtime_explicit=1
- shift 2
- ;;
- --runtime=*)
- RUNTIME="${1#--runtime=}"
- runtime_explicit=1
- shift
- ;;
- --print-launch)
- print_launch=1
- shift
+# Argument parsing + mode dispatch. Wrapped so the file can be sourced (by the
+# launcher's bats tests) to exercise individual functions without running a
+# real launch. When executed as a program, BASH_SOURCE[0] equals $0 and the
+# dispatch runs exactly as before; when sourced, it's skipped.
+main() {
+ print_launch=""
+ runtime_explicit="${AI_RUNTIME:+1}"
+ while [ $# -gt 0 ]; do
+ case "$1" in
+ -h|--help)
+ usage
+ ;;
+ --runtime)
+ [ -z "${2:-}" ] && { echo "ai: --runtime needs a value — valid runtimes: claude, codex, local" >&2; exit 2; }
+ RUNTIME="$2"
+ runtime_explicit=1
+ shift 2
+ ;;
+ --runtime=*)
+ RUNTIME="${1#--runtime=}"
+ runtime_explicit=1
+ shift
+ ;;
+ --print-launch)
+ print_launch=1
+ shift
+ ;;
+ --print-runtimes)
+ build_runtime_choices
+ exit 0
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ resolve_agent_cmd
+
+ if [ -n "$print_launch" ]; then
+ [ $# -eq 0 ] && { echo "ai: --print-launch needs a project directory" >&2; exit 2; }
+ print_launch_mode "$1"
+ fi
+
+ case "${1:-}" in
+ --attach)
+ check_deps
+ attach_mode
;;
- --print-runtimes)
- build_runtime_choices
- exit 0
+ "")
+ # Bare `ai`: pick the agent first (skipped when --runtime or AI_RUNTIME
+ # already chose), then the familiar project multi-select.
+ if [ -z "$runtime_explicit" ]; then
+ pick_runtime || exit 0
+ fi
+ check_deps
+ multi_mode
;;
*)
- break
+ check_deps
+ for arg in "$@"; do
+ single_mode "$arg"
+ done
;;
esac
-done
-
-resolve_agent_cmd
+}
-if [ -n "$print_launch" ]; then
- [ $# -eq 0 ] && { echo "ai: --print-launch needs a project directory" >&2; exit 2; }
- print_launch_mode "$1"
+if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
+ main "$@"
fi
-
-case "${1:-}" in
- --attach)
- check_deps
- attach_mode
- ;;
- "")
- # Bare `ai`: pick the agent first (skipped when --runtime or AI_RUNTIME
- # already chose), then the familiar project multi-select.
- if [ -z "$runtime_explicit" ]; then
- pick_runtime || exit 0
- fi
- check_deps
- multi_mode
- ;;
- *)
- check_deps
- for arg in "$@"; do
- single_mode "$arg"
- done
- ;;
-esac