orient-project
#!/bin/bash
# Describes a project root: lists notable files, inlines orientation docs,
# and detects an associated "meta" repo (private notes companion).
#
# Usage: orient-project [PATH]
# PATH defaults to the git toplevel of cwd, or cwd itself if not in a repo.
#
# Emits a markdown fragment intended to sit under an h3 or smaller.
# Inlined file contents are wrapped in <file path="..."> XML tags so they
# carry their own framing and don't fight the surrounding markdown.
#
# Never fully silent (a project root always exists), but individual
# subsections (meta repo, etc.) are omitted when there's nothing to say.
set -euo pipefail
# Resolve the project root.
# Delegated to `find-project-root` so the "what is a project" decision
# lives in exactly one place (`find-project-root` is strict: git toplevel
# or nothing). We add a cwd fallback here because this script wants to
# produce *some* orientation even outside a project context.
self_dir=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
find_root="$self_dir/find-project-root"
if [ $# -ge 1 ]; then
root=$(cd "$1" && pwd)
elif root=$("$find_root" 2>/dev/null); then
:
else
root="$PWD"
fi
echo "- project root: \`${root}\`"
# Orientation docs to inline if present.
# Case-insensitive name match against a small set of conventional filenames,
# with a few common extensions. `.editorconfig` is included by exact name.
# We deliberately don't recurse: this is a root-orientation probe.
shopt -s nullglob nocaseglob
orient_files=()
for base in README CONTRIBUTING HACKME AGENTS CLAUDE; do
for candidate in "$root/$base" "$root/$base".md "$root/$base".rst "$root/$base".txt; do
if [ -f "$candidate" ]; then
orient_files+=("$candidate")
fi
done
done
shopt -u nocaseglob
if [ -f "$root/.editorconfig" ]; then
orient_files+=("$root/.editorconfig")
fi
# Inline the orientation docs, each wrapped in its own <file> tag.
# The path attribute is repo-relative for readability.
if [ ${#orient_files[@]} -gt 0 ]; then
echo
echo "#### Orientation files"
echo
for f in "${orient_files[@]}"; do
rel="${f#$root/}"
echo "<file path=\"${rel}\">"
cat "$f"
echo "</file>"
echo
done
fi
# List remaining root entries (depth 1, no dotfiles, excluding files we inlined).
# This is a sanity probe, not an inventory tool, so cap at 50 entries.
# (If a project root has more than 50 visible entries at depth 1,
# something else is going on and a fuller listing isn't the right tool anyway.)
mapfile -t root_entries < <(
find "$root" -mindepth 1 -maxdepth 1 -not -name '.*' -printf '%f\n' | sort
)
# Build a lookup of inlined basenames so we can skip them in the listing.
declare -A inlined=()
for f in "${orient_files[@]}"; do
inlined["$(basename "$f")"]=1
done
remaining=()
for entry in "${root_entries[@]}"; do
if [ -z "${inlined[$entry]:-}" ]; then
remaining+=("$entry")
fi
done
if [ ${#remaining[@]} -gt 0 ]; then
echo
echo "#### Other root entries"
echo
count=0
for entry in "${remaining[@]}"; do
if [ $count -ge 50 ]; then
echo "- ... ($((${#remaining[@]} - 50)) more not shown)"
break
fi
if [ -d "$root/$entry" ]; then
echo "- \`${entry}/\`"
else
echo "- \`${entry}\`"
fi
count=$((count + 1))
done
fi
# Meta repo detection.
# Two conventions, checked in order:
# 1. `<root>/.meta` -- if it's a symlink, or a non-empty directory.
# (An empty real directory falls through; it's probably stale state.)
# 2. `<root>/../<basename>.meta/` -- sibling directory next to the repo.
# The first hit wins; we don't try to merge.
meta=""
# Resolve symlinks (`pwd -P`) so the reported path points to where files live.
if [ -L "$root/.meta" ] && [ -d "$root/.meta" ]; then
meta=$(cd "$root/.meta" && pwd -P)
elif [ -d "$root/.meta" ] && [ -n "$(ls -A "$root/.meta" 2>/dev/null)" ]; then
meta=$(cd "$root/.meta" && pwd -P)
fi
if [ -z "$meta" ]; then
parent=$(dirname "$root")
base=$(basename "$root")
sibling="$parent/$base.meta"
if [ -d "$sibling" ]; then
meta=$(cd "$sibling" && pwd)
fi
fi
if [ -n "$meta" ]; then
echo
echo "#### Meta repo"
echo
echo "- path: \`${meta}\`"
# Directory layout: shallow listing of subdirs to convey structure.
# Prune dotdirs entirely (don't descend into them) so noise like `.git/objects/`
# doesn't leak in via the maxdepth-2 walk.
mapfile -t meta_dirs < <(
find "$meta" -mindepth 1 -maxdepth 2 \
\( -name '.*' -prune \) -o \
\( -type d -printf '%P\n' \) \
| sort
)
if [ ${#meta_dirs[@]} -gt 0 ]; then
echo "- directory layout:"
for d in "${meta_dirs[@]}"; do
echo " - \`${d}/\`"
done
fi
# Recent worklogs: filenames with a YYYY-MM-DD prefix.
# Look first under `worklogs/`, then fall back to anywhere in the meta repo.
# Sort by the date prefix (lexicographic = chronological for ISO dates),
# show the 5 most recent.
if [ -d "$meta/worklogs" ]; then
worklog_search_root="$meta/worklogs"
else
worklog_search_root="$meta"
fi
mapfile -t worklogs < <(
find "$worklog_search_root" -type f -name '????-??-??*' -printf '%P\n' \
| sort -r \
| head -n 5
)
if [ ${#worklogs[@]} -gt 0 ]; then
echo "- recent worklogs (under \`${worklog_search_root#$meta/}\`):"
for w in "${worklogs[@]}"; do
echo " - \`${w}\`"
done
fi
fi
orient-project · 5.1 KiB · 9920325da1