From 9033b275985a4d8f9663202a6cbb98bf3338fa81 Mon Sep 17 00:00:00 2001 From: Logan Gagne Date: Fri, 9 Oct 2026 12:20:40 -0400 Subject: [PATCH] feat(git-wait): add -C DIR to target another repo Lets callers run git-wait against a repo other than the current directory without a caller-side cd. The cd happens inside the script's own process, so platform detection, pr wait, run watch and the gh/tea calls all follow the target repo. - utils/git-wait: global -C DIR / --repo-dir DIR flag, validated before platform detection - resolve the script path before the cd so --help still works - git-tools SKILL.md: document targeting another repo - tests/git-wait/test-target-repo.sh: cover both platforms, alias, subcommand composition and validation errors - re-vendor git-wait into git-tools and session - bump git-tools to 3.4.0 and session to 4.9.2 (claude and copilot) Co-Authored-By: Claude Sonnet 5.5 --- .../git-tools/.claude-plugin/plugin.json | 2 +- plugins-claude/git-tools/scripts/git-wait | 27 ++++- .../git-tools/skills/git-wait/SKILL.md | 12 ++ .../session/.claude-plugin/plugin.json | 2 +- plugins-claude/session/scripts/git-wait | 27 ++++- .../git-tools/.claude-plugin/plugin.json | 2 +- .../session/.claude-plugin/plugin.json | 2 +- tests/git-wait/test-target-repo.sh | 108 ++++++++++++++++++ utils/git-wait | 27 ++++- 9 files changed, 202 insertions(+), 7 deletions(-) create mode 100755 tests/git-wait/test-target-repo.sh diff --git a/plugins-claude/git-tools/.claude-plugin/plugin.json b/plugins-claude/git-tools/.claude-plugin/plugin.json index 01a85fb..fd79269 100644 --- a/plugins-claude/git-tools/.claude-plugin/plugin.json +++ b/plugins-claude/git-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "git-tools", - "version": "3.3.0", + "version": "3.4.0", "description": "GitHub and Gitea tooling — platform detection plus blocking waiters for PR merges and CI runs, plus a ship orchestrator that drives the full branch/commit/push/PR/watch/cleanup lifecycle using gh/tea directly", "author": { "name": "Logan Gagne" diff --git a/plugins-claude/git-tools/scripts/git-wait b/plugins-claude/git-tools/scripts/git-wait index 71eb76a..335f658 100755 --- a/plugins-claude/git-tools/scripts/git-wait +++ b/plugins-claude/git-tools/scripts/git-wait @@ -15,6 +15,11 @@ # job-level failures masked by a run-level success; `tea` has no watcher. # # Usage: +# git-wait [-C DIR] ... +# Operate on the git repo at DIR instead of the current directory (like +# `git -C`). Must come before the command. gh/tea resolve the repo from the +# working directory, so this retargets every call without a caller-side cd. +# # git-wait platform # Echo "github" or "gitea" for the current repo's origin remote. # @@ -115,6 +120,26 @@ cli_json() { echo "$raw" | jq "${jq_flags[@]+"${jq_flags[@]}"}" "$filter" } +# --------------------------------------------------------------------------- +# Target repo (-C DIR) +# +# Handled before platform detection. The cd happens inside this script's own +# process, so the caller's working directory is never touched. +# --------------------------------------------------------------------------- + +if [[ "${1:-}" == "-C" || "${1:-}" == "--repo-dir" ]]; then + [[ $# -gt 1 && -n "$2" ]] || die_usage "$1 requires a directory" + target_dir="$2" + [[ -d "$target_dir" ]] || die_usage "$1: not a directory: $target_dir" + git -C "$target_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || die_usage "$1: not a git repository: $target_dir" + # Resolve the script path first: a relative $0 breaks once we cd (help reads it). + SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" + cd "$target_dir" + shift 2 +fi +SELF="${SELF:-$0}" + # --------------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------------- @@ -1425,7 +1450,7 @@ case "$cmd:$sub" in ;; --help:|-h:|:) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,/^$/p' "$SELF" | sed 's/^# \{0,1\}//' exit 0 ;; diff --git a/plugins-claude/git-tools/skills/git-wait/SKILL.md b/plugins-claude/git-tools/skills/git-wait/SKILL.md index 8a0f673..c46a3a1 100644 --- a/plugins-claude/git-tools/skills/git-wait/SKILL.md +++ b/plugins-claude/git-tools/skills/git-wait/SKILL.md @@ -114,6 +114,18 @@ Exit codes: `0` terminal state reached (check `status:` for pass/fail), `1` usage error, `2` timeout, `3` platform detection failure or no PR/workflow found, `4` command failed. +## Targeting another repo + +Put `-C DIR` before the command to run against a repo other than the current +directory — use it instead of `cd`: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/scripts/git-wait -C /path/to/other-repo run watch --branch NAME +``` + +Only `git-wait` honors `-C`; direct `gh`/`tea` calls against another repo still +need their own `-R OWNER/REPO` / `--repo` flag. + ## gh vs. tea command map Verified against `gh` 2.100.0 and `tea` 0.15.1. Don't guess beyond this diff --git a/plugins-claude/session/.claude-plugin/plugin.json b/plugins-claude/session/.claude-plugin/plugin.json index a348c7f..1567db3 100644 --- a/plugins-claude/session/.claude-plugin/plugin.json +++ b/plugins-claude/session/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "session", - "version": "4.9.1", + "version": "4.9.2", "dependencies": ["git-tools"], "description": "Work session management — issue-driven and freeform doors sharing an explore-then-plan spine, with multi-agent orchestration and a wrap-up skill that reviews, sweeps issues, and ships", "author": { diff --git a/plugins-claude/session/scripts/git-wait b/plugins-claude/session/scripts/git-wait index 71eb76a..335f658 100755 --- a/plugins-claude/session/scripts/git-wait +++ b/plugins-claude/session/scripts/git-wait @@ -15,6 +15,11 @@ # job-level failures masked by a run-level success; `tea` has no watcher. # # Usage: +# git-wait [-C DIR] ... +# Operate on the git repo at DIR instead of the current directory (like +# `git -C`). Must come before the command. gh/tea resolve the repo from the +# working directory, so this retargets every call without a caller-side cd. +# # git-wait platform # Echo "github" or "gitea" for the current repo's origin remote. # @@ -115,6 +120,26 @@ cli_json() { echo "$raw" | jq "${jq_flags[@]+"${jq_flags[@]}"}" "$filter" } +# --------------------------------------------------------------------------- +# Target repo (-C DIR) +# +# Handled before platform detection. The cd happens inside this script's own +# process, so the caller's working directory is never touched. +# --------------------------------------------------------------------------- + +if [[ "${1:-}" == "-C" || "${1:-}" == "--repo-dir" ]]; then + [[ $# -gt 1 && -n "$2" ]] || die_usage "$1 requires a directory" + target_dir="$2" + [[ -d "$target_dir" ]] || die_usage "$1: not a directory: $target_dir" + git -C "$target_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || die_usage "$1: not a git repository: $target_dir" + # Resolve the script path first: a relative $0 breaks once we cd (help reads it). + SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" + cd "$target_dir" + shift 2 +fi +SELF="${SELF:-$0}" + # --------------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------------- @@ -1425,7 +1450,7 @@ case "$cmd:$sub" in ;; --help:|-h:|:) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,/^$/p' "$SELF" | sed 's/^# \{0,1\}//' exit 0 ;; diff --git a/plugins-copilot/git-tools/.claude-plugin/plugin.json b/plugins-copilot/git-tools/.claude-plugin/plugin.json index 01a85fb..fd79269 100644 --- a/plugins-copilot/git-tools/.claude-plugin/plugin.json +++ b/plugins-copilot/git-tools/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "git-tools", - "version": "3.3.0", + "version": "3.4.0", "description": "GitHub and Gitea tooling — platform detection plus blocking waiters for PR merges and CI runs, plus a ship orchestrator that drives the full branch/commit/push/PR/watch/cleanup lifecycle using gh/tea directly", "author": { "name": "Logan Gagne" diff --git a/plugins-copilot/session/.claude-plugin/plugin.json b/plugins-copilot/session/.claude-plugin/plugin.json index b3cba12..39b4791 100644 --- a/plugins-copilot/session/.claude-plugin/plugin.json +++ b/plugins-copilot/session/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "session", - "version": "4.9.1", + "version": "4.9.2", "description": "Work session management — issue-driven and freeform doors sharing an explore-then-plan spine, with multi-agent orchestration and a wrap-up skill that reviews, sweeps issues, and ships", "author": { "name": "Logan Gagne" diff --git a/tests/git-wait/test-target-repo.sh b/tests/git-wait/test-target-repo.sh new file mode 100755 index 0000000..a612466 --- /dev/null +++ b/tests/git-wait/test-target-repo.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# test-target-repo.sh — git-wait -C DIR retargets every call at another repo. +# +# Two real throwaway repos (GitHub-hosted and Gitea-hosted origins) plus a tea +# login stub on PATH. git-wait is always launched from a third, unrelated +# directory so a pass proves -C — not the working directory — chose the repo. +# +# Usage: bash tests/git-wait/test-target-repo.sh [filter] + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +GIT_WAIT="$SCRIPT_DIR/../../utils/git-wait" + +PASS=0 +FAIL=0 +FILTER="${1:-}" + +WORK="" +cleanup() { [[ -n "$WORK" ]] && rm -rf "$WORK"; } +trap cleanup EXIT +WORK=$(mktemp -d) + +pass() { + printf " \033[32m✓\033[0m %s\n" "$1" + ((PASS++)) || true +} +fail() { + printf " \033[31m✗\033[0m %s (%s)\n" "$1" "$2" + ((FAIL++)) || true +} +skip_filter() { [[ -n "$FILTER" ]] && ! echo "$1" | grep -qi "$FILTER"; } + +mkdir -p "$WORK/bin" "$WORK/github-repo" "$WORK/gitea-repo" "$WORK/elsewhere" "$WORK/not-a-repo" +git init -q "$WORK/github-repo" +git -C "$WORK/github-repo" remote add origin "https://github.com/owner/repo.git" +git init -q "$WORK/gitea-repo" +git -C "$WORK/gitea-repo" remote add origin "https://git.example.test/owner/repo.git" + +# Only the Gitea host is a configured tea login; anything else falls to gh. +cat >"$WORK/bin/tea" <<'EOS' +#!/usr/bin/env bash +case "$1 $2 $3" in + "login list --output") echo '[{"url":"https://git.example.test"}]' ;; + *) exit 1 ;; +esac +EOS +chmod +x "$WORK/bin/tea" +# Platform detection never calls gh, but the script requires it on PATH. +printf '#!/usr/bin/env bash\nexit 1\n' >"$WORK/bin/gh" +chmod +x "$WORK/bin/gh" + +# Run git-wait from an unrelated cwd; sets OUT, ERR, CODE. +gw() { + CODE=0 + OUT=$(cd "$WORK/elsewhere" && PATH="$WORK/bin:$PATH" bash "$GIT_WAIT" "$@" 2>"$WORK/stderr") || CODE=$? + ERR=$(cat "$WORK/stderr") +} + +echo "── -C DIR ──" + +label="-C picks the GitHub repo regardless of cwd" +if ! skip_filter "$label"; then + gw -C "$WORK/github-repo" platform + if [[ "$CODE" == "0" && "$OUT" == "github" ]]; then pass "$label"; else fail "$label" "exit=$CODE out=$OUT err=$ERR"; fi +fi + +label="-C picks the Gitea repo regardless of cwd" +if ! skip_filter "$label"; then + gw -C "$WORK/gitea-repo" platform + if [[ "$CODE" == "0" && "$OUT" == "gitea" ]]; then pass "$label"; else fail "$label" "exit=$CODE out=$OUT err=$ERR"; fi +fi + +label="--repo-dir is an alias for -C" +if ! skip_filter "$label"; then + gw --repo-dir "$WORK/gitea-repo" platform + if [[ "$CODE" == "0" && "$OUT" == "gitea" ]]; then pass "$label"; else fail "$label" "exit=$CODE out=$OUT err=$ERR"; fi +fi + +label="-C composes with a subcommand (run watch usage error comes from the target)" +if ! skip_filter "$label"; then + gw -C "$WORK/github-repo" run watch --branch feat --settle 30 + if [[ "$CODE" == "1" && "$ERR" == *"--settle requires --sha"* ]]; then pass "$label"; else fail "$label" "exit=$CODE err=$ERR"; fi +fi + +echo "── -C validation ──" + +label="nonexistent directory is a usage error (exit 1)" +if ! skip_filter "$label"; then + gw -C "$WORK/missing" platform + if [[ "$CODE" == "1" && "$ERR" == *"not a directory"* ]]; then pass "$label"; else fail "$label" "exit=$CODE err=$ERR"; fi +fi + +label="non-git directory is a usage error (exit 1)" +if ! skip_filter "$label"; then + gw -C "$WORK/not-a-repo" platform + if [[ "$CODE" == "1" && "$ERR" == *"not a git repository"* ]]; then pass "$label"; else fail "$label" "exit=$CODE err=$ERR"; fi +fi + +label="-C without a value is a usage error (exit 1)" +if ! skip_filter "$label"; then + gw -C + if [[ "$CODE" == "1" && "$ERR" == *"requires a directory"* ]]; then pass "$label"; else fail "$label" "exit=$CODE err=$ERR"; fi +fi + +echo "" +echo "Total: $((PASS + FAIL)) PASS: $PASS FAIL: $FAIL" +[[ "$FAIL" -eq 0 ]] diff --git a/utils/git-wait b/utils/git-wait index 71eb76a..335f658 100755 --- a/utils/git-wait +++ b/utils/git-wait @@ -15,6 +15,11 @@ # job-level failures masked by a run-level success; `tea` has no watcher. # # Usage: +# git-wait [-C DIR] ... +# Operate on the git repo at DIR instead of the current directory (like +# `git -C`). Must come before the command. gh/tea resolve the repo from the +# working directory, so this retargets every call without a caller-side cd. +# # git-wait platform # Echo "github" or "gitea" for the current repo's origin remote. # @@ -115,6 +120,26 @@ cli_json() { echo "$raw" | jq "${jq_flags[@]+"${jq_flags[@]}"}" "$filter" } +# --------------------------------------------------------------------------- +# Target repo (-C DIR) +# +# Handled before platform detection. The cd happens inside this script's own +# process, so the caller's working directory is never touched. +# --------------------------------------------------------------------------- + +if [[ "${1:-}" == "-C" || "${1:-}" == "--repo-dir" ]]; then + [[ $# -gt 1 && -n "$2" ]] || die_usage "$1 requires a directory" + target_dir="$2" + [[ -d "$target_dir" ]] || die_usage "$1: not a directory: $target_dir" + git -C "$target_dir" rev-parse --is-inside-work-tree >/dev/null 2>&1 \ + || die_usage "$1: not a git repository: $target_dir" + # Resolve the script path first: a relative $0 breaks once we cd (help reads it). + SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")" + cd "$target_dir" + shift 2 +fi +SELF="${SELF:-$0}" + # --------------------------------------------------------------------------- # Platform detection # --------------------------------------------------------------------------- @@ -1425,7 +1450,7 @@ case "$cmd:$sub" in ;; --help:|-h:|:) - sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,/^$/p' "$SELF" | sed 's/^# \{0,1\}//' exit 0 ;;