diff --git a/lib/commands/ai.sh b/lib/commands/ai.sh index 9549d03..0f7caa9 100644 --- a/lib/commands/ai.sh +++ b/lib/commands/ai.sh @@ -39,5 +39,13 @@ cmd_ai() { log_info "Directory: $worktree_path" log_info "Branch: $branch" - ai_start "$worktree_path" "${ai_args[@]}" + # Run postCd hooks + AI in a subshell so hook env vars propagate to AI + ( + cd "$worktree_path" || exit 1 + run_hooks_export "postCd" \ + REPO_ROOT="$repo_root" \ + WORKTREE_PATH="$worktree_path" \ + BRANCH="$branch" + ai_start "$worktree_path" "${ai_args[@]}" + ) } \ No newline at end of file diff --git a/lib/commands/create.sh b/lib/commands/create.sh index f325e79..f8e89f9 100644 --- a/lib/commands/create.sh +++ b/lib/commands/create.sh @@ -192,7 +192,7 @@ cmd_create() { # Auto-launch editor/AI or show next steps [ "$open_editor" -eq 1 ] && { _auto_launch_editor "$worktree_path" || true; } - [ "$start_ai" -eq 1 ] && { _auto_launch_ai "$worktree_path" || true; } + [ "$start_ai" -eq 1 ] && { _auto_launch_ai "$worktree_path" "$repo_root" "$branch_name" || true; } if [ "$open_editor" -eq 0 ] && [ "$start_ai" -eq 0 ]; then _post_create_next_steps "$branch_name" "$folder_name" "$folder_override" "$repo_root" "$base_dir" "$prefix" fi diff --git a/lib/hooks.sh b/lib/hooks.sh index c291974..75184d8 100644 --- a/lib/hooks.sh +++ b/lib/hooks.sh @@ -84,3 +84,42 @@ run_hooks_in() { return $result } + +# Run hooks in current shell without subshell isolation +# Env vars set by hooks (e.g., source ./vars.sh) persist in the calling shell. +# IMPORTANT: Call from within a subshell to avoid polluting the main script. +# Usage: run_hooks_export phase [env_vars...] +# Example: ( cd "$dir" && run_hooks_export postCd REPO_ROOT="$root" ) +run_hooks_export() { + local phase="$1" + shift + + local hooks + hooks=$(cfg_get_all "gtr.hook.$phase" "hooks.$phase") + + if [ -z "$hooks" ]; then + return 0 + fi + + log_step "Running $phase hooks..." + + # Export env vars so hooks and child processes can see them + local kv + for kv in "$@"; do + # shellcheck disable=SC2163 + export "$kv" + done + + local hook_count=0 + while IFS= read -r hook; do + [ -z "$hook" ] && continue + + hook_count=$((hook_count + 1)) + log_info "Hook $hook_count: $hook" + + # eval directly (no subshell) so exports persist + eval "$hook" || log_warn "Hook $hook_count failed (continuing)" + done < [repo_root] [branch] +# When repo_root and branch are provided, runs postCd hooks before launch. _auto_launch_ai() { local worktree_path="$1" + local repo_root="${2:-}" branch="${3:-}" local ai_tool ai_tool=$(_cfg_ai_default) if [ "$ai_tool" = "none" ]; then @@ -50,6 +53,17 @@ _auto_launch_ai() { else load_ai_adapter "$ai_tool" || return 1 log_step "Starting $ai_tool..." - ai_start "$worktree_path" || log_warn "Failed to start AI tool" + if [ -n "$repo_root" ] && [ -n "$branch" ]; then + ( + cd "$worktree_path" || exit 1 + run_hooks_export "postCd" \ + REPO_ROOT="$repo_root" \ + WORKTREE_PATH="$worktree_path" \ + BRANCH="$branch" + ai_start "$worktree_path" + ) || log_warn "Failed to start AI tool" + else + ai_start "$worktree_path" || log_warn "Failed to start AI tool" + fi fi } diff --git a/tests/hooks.bats b/tests/hooks.bats index d3ee690..5df9c77 100644 --- a/tests/hooks.bats +++ b/tests/hooks.bats @@ -88,3 +88,49 @@ teardown() { run_hooks postCreate REPO_ROOT="$TEST_REPO" BRANCH="test-branch" [ "$(cat "$TEST_REPO/vars")" = "$TEST_REPO|test-branch" ] } + +# ── run_hooks_export tests ─────────────────────────────────────────────────── + +@test "run_hooks_export returns 0 when no hooks configured" { + run run_hooks_export postCd REPO_ROOT="$TEST_REPO" + [ "$status" -eq 0 ] +} + +@test "run_hooks_export executes hook" { + git config --add gtr.hook.postCd 'touch "$REPO_ROOT/hook-ran"' + (cd "$TEST_REPO" && run_hooks_export postCd REPO_ROOT="$TEST_REPO") + [ -f "$TEST_REPO/hook-ran" ] +} + +@test "run_hooks_export env vars propagate to child processes" { + git config --add gtr.hook.postCd 'export MY_CUSTOM_VAR="from-hook"' + # Run hook then check env in same subshell — simulates ai_start inheriting env + result=$( + cd "$TEST_REPO" + run_hooks_export postCd REPO_ROOT="$TEST_REPO" + echo "$MY_CUSTOM_VAR" + ) + [ "$result" = "from-hook" ] +} + +@test "run_hooks_export continues after hook failure" { + git config --add gtr.hook.postCd "false" + git config --add gtr.hook.postCd 'touch "$REPO_ROOT/second-ran"' + (cd "$TEST_REPO" && run_hooks_export postCd REPO_ROOT="$TEST_REPO") || true + [ -f "$TEST_REPO/second-ran" ] +} + +@test "run_hooks_export passes REPO_ROOT WORKTREE_PATH BRANCH" { + git config --add gtr.hook.postCd 'echo "$REPO_ROOT|$WORKTREE_PATH|$BRANCH" > "$REPO_ROOT/env-check"' + (cd "$TEST_REPO" && run_hooks_export postCd \ + REPO_ROOT="$TEST_REPO" \ + WORKTREE_PATH="/tmp/wt" \ + BRANCH="my-branch") + [ "$(cat "$TEST_REPO/env-check")" = "$TEST_REPO|/tmp/wt|my-branch" ] +} + +@test "run_hooks_export does not leak env to parent shell" { + git config --add gtr.hook.postCd 'export LEAK_TEST="leaked"' + (cd "$TEST_REPO" && run_hooks_export postCd REPO_ROOT="$TEST_REPO") + [ -z "${LEAK_TEST:-}" ] +}