#!/bin/bash # Detects project entrypoint systems (Makefile etc.) and surfaces their # targets so an agent can act without an exploratory roundtrip. # # Usage: orient-entrypoints [PATH] # PATH defaults to the git toplevel of cwd, or cwd itself if not in a repo. # # Silent (no output, exit 0) when no entrypoint system is detected. # Each detected system emits an h4 heading; intended to sit under an h3. # # Hard rule: parse only, never invoke. # Repos can be untrusted content (e.g. freshly cloned from elsewhere); # the existence of files in the repo must not translate into code execution. # Any future addition here must follow the same rule. set -euo pipefail # Resolve the project root (mirrors orient-project). if [ $# -ge 1 ]; then root=$(cd "$1" && pwd) elif root=$(git rev-parse --show-toplevel 2>/dev/null); then : else root="$PWD" fi # Cap on how many targets we'll list per system. # Generous; mirrors orient-project's listing cap. MAX_TARGETS=50 # --- Makefile ------------------------------------------------------------- # # Search order matches GNU make's own (GNUmakefile, makefile, Makefile). # We only consider the first one found, the same as make would. makefile="" for candidate in GNUmakefile makefile Makefile; do if [ -f "$root/$candidate" ]; then makefile="$root/$candidate" break fi done if [ -n "$makefile" ]; then rel="${makefile#$root/}" echo "#### \`${rel}\`" echo # Two-pass parse, in order of preference: # # 1. Annotated targets: `name: [deps] ## description`. # A widespread cargo-culted convention (often paired with a `help` target # that greps for the same pattern). When present, treat it as the # author's curated entrypoint surface. # # 2. Bare targets: `name:` or `name: deps`. # Fallback when no annotations are found. Best-effort: filters out # pattern rules (`%`), variable assignments (`:=`, `=`, etc.), and # dotted internal targets (`.PHONY`, `.SUFFIXES`, ...) via the leading # char class. # # Both passes are pure regex over the file; we never expand variables or # follow includes. That means a Makefile that defines targets via include # or eval will look empty here -- and that's the correct, safe behavior. annotated=$(grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*:[^=]*##' "$makefile" || true) if [ -n "$annotated" ]; then echo "Annotated targets (\`name: ## description\` convention):" echo printf '%s\n' "$annotated" \ | awk -F'##' '{ split($1, a, ":"); name = a[1]; gsub(/^[ \t]+|[ \t]+$/, "", name); desc = $2; gsub(/^[ \t]+|[ \t]+$/, "", desc); printf "- `%s` -- %s\n", name, desc; }' \ | head -n "$MAX_TARGETS" total=$(printf '%s\n' "$annotated" | wc -l) if [ "$total" -gt "$MAX_TARGETS" ]; then echo "- ... ($((total - MAX_TARGETS)) more not shown)" fi else # Bare-target parse. # Regex requires `:` (or `::`) followed by space or end-of-line, which # naturally excludes `:=` and other assignment forms. mapfile -t targets < <( grep -E '^[a-zA-Z0-9_][a-zA-Z0-9_.-]*[[:space:]]*::?([[:space:]]|$)' "$makefile" \ | grep -v '%' \ | sed -E 's/^([a-zA-Z0-9_][a-zA-Z0-9_.-]*).*/\1/' \ | awk '!seen[$0]++' \ || true ) if [ ${#targets[@]} -gt 0 ]; then echo "Targets (parsed; no \`## description\` annotations found):" echo n=0 for t in "${targets[@]}"; do if [ "$n" -ge "$MAX_TARGETS" ]; then echo "- ... ($((${#targets[@]} - MAX_TARGETS)) more not shown)" break fi echo "- \`$t\`" n=$((n + 1)) done else # Makefile present but yielded no targets we recognize. # Useful signal in itself: tells the agent "go read it". echo "(No targets parsed; the Makefile may define targets via include or eval.)" fi fi fi # --- Future entrypoint systems -------------------------------------------- # # Add detection blocks here for justfile, Taskfile.yml, scripts/, etc. # Same rules apply: parse only, never invoke. Each block stays silent if # its system isn't present, and emits an h4 heading when it is.