compass

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