orient-entrypoints
#!/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.
orient-entrypoints · 4.0 KiB · a4ee40002b