#!/usr/bin/env bash # Shared helpers for the git-manager skill. Sourced by git_cmd.sh, not # meant to be executed directly. CONFIG_FILE="$HOME/.agent-skills/git-manager/config.json" # Subcommands this skill is allowed to run. Anything not in this list is # rejected outright by git_cmd.sh, regardless of what the caller asks for. ALLOWED_SUBCOMMANDS=(status log diff show fetch remote branch checkout add commit push pull tag) is_allowed_subcommand() { local sub="$1" local allowed for allowed in "${ALLOWED_SUBCOMMANDS[@]}"; do [ "$sub" = "$allowed" ] && return 0 done return 1 } # Resolve a repo's absolute path. Only ever looks at configured repos — # never falls back to the current working directory, regardless of # whether it happens to be a git repo itself. This is deliberate: the # current directory being a git repo is not a signal this script acts on. # - If a name is given, look it up in the config file's "repos" array. # - If no name is given, look at the config: exactly one registered repo # is used automatically; zero or more than one is an error requiring an # explicit --repo. # Never accepts a raw path from the caller directly — only ever a name # resolved through the config file, so the whitelisted script can't be # pointed at an arbitrary directory outside what's configured. resolve_repo_path() { local repo_name="${1:-}" if [ ! -f "$CONFIG_FILE" ]; then echo "Error: no configuration found at $CONFIG_FILE. Register at least one repo first (run setup, or ask to register one)." >&2 exit 1 fi if ! command -v python3 >/dev/null 2>&1; then echo "Error: python3 is required to parse the config file." >&2 exit 1 fi python3 - "$CONFIG_FILE" "$repo_name" << 'PYEOF' import json, sys config_file, repo_name = sys.argv[1], sys.argv[2] with open(config_file) as f: cfg = json.load(f) repos = cfg.get("repos", []) if repo_name: match = [r for r in repos if r.get("name") == repo_name] if not match: names = ", ".join(r.get("name", "?") for r in repos) or "(none)" sys.stderr.write(f"Error: no repo named '{repo_name}' in config. Registered: {names}\n") sys.exit(1) print(match[0]["path"]) elif len(repos) == 1: print(repos[0]["path"]) elif len(repos) == 0: sys.stderr.write("Error: no repos registered in config. Register at least one first.\n") sys.exit(1) else: names = ", ".join(r.get("name", "?") for r in repos) sys.stderr.write(f"Error: multiple repos configured ({names}); specify one with --repo .\n") sys.exit(1) PYEOF } # Validate that a resolved path is a real, existing directory that is # actually inside a git work tree. Prints the canonical repo root. canonicalize_and_check_repo() { local path="$1" local real real=$(realpath -e "$path" 2>/dev/null) || { echo "Error: path '$path' does not exist." >&2 exit 1 } if [ ! -d "$real" ]; then echo "Error: path '$real' is not a directory." >&2 exit 1 fi if ! git -C "$real" rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo "Error: '$real' is not inside a git work tree." >&2 exit 1 fi git -C "$real" rev-parse --show-toplevel }