| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- #!/usr/bin/env bash
- # Bootstrap an ADR directory in a repo adopting Architecture Decision Records cold.
- #
- # Usage: adr-init.sh [--dir DIR] [--first-title "TEXT"] [--dry-run] [--force]
- # Input: argv flags only (no stdin).
- # Output: stdout = paths created (or, under --dry-run, the actions it would take).
- # Data only.
- # Stderr: headers, reminders, warnings, errors.
- # Exit: 0 created (or dry-run rendered), 2 usage, 5 precondition
- # (dir already contains ADR-*.md and --force not given)
- #
- # Creates <dir> if missing, scaffolds a lint-clean ADR-001 (by invoking adr-new.sh
- # so it shares the canonical template), and writes a short generated <dir>/README.md
- # pointing at the directory-as-index discipline. Refuses to run in a directory that
- # already holds ADRs unless --force. Atomic writes; never clobbers existing files.
- #
- # Examples:
- # adr-init.sh
- # adr-init.sh --dir docs/decisions --first-title "Adopt ADRs"
- # adr-init.sh --first-title "OAuth-only auth" --dry-run
- set -uo pipefail
- readonly EX_OK=0 EX_USAGE=2 EX_PRECOND=5
- # Terminal design system (skills/_lib/term.sh). stdout = paths created (data); the
- # bootstrap summary frames on stderr, so detect color on fd 2. Degrade to plain
- # stderr lines if the shared lib is unreachable.
- __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
- if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
- else
- term_panel_open() { :; }; term_panel_close() { :; }; term_panel_vert() { :; }
- term_status_row() { shift; printf ' - %s %s\n' "$1" "${2:-}"; }
- term_color() { shift; printf '%s' "$*"; }; TERM_DOT="|"
- fi
- DIR="docs/adr"
- FIRST_TITLE="Record architecture decisions"
- DRY_RUN=0
- FORCE=0
- HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- ADR_NEW="$HERE/adr-new.sh"
- usage() {
- cat <<'EOF'
- adr-init.sh — bootstrap an ADR directory in a repo adopting ADRs cold.
- Usage:
- adr-init.sh [--dir DIR] [--first-title "TEXT"] [--dry-run] [--force]
- Options:
- --dir DIR ADR directory to create/populate (default: docs/adr)
- --first-title TEXT Title for the scaffolded ADR-001
- (default: "Record architecture decisions")
- --dry-run Print what would happen; write nothing.
- --force Proceed even if DIR already contains ADR-*.md.
- -h, --help Show this help and exit 0.
- Exit codes:
- 0 created (or dry-run) 2 usage 5 dir already has ADRs (without --force)
- Examples:
- adr-init.sh
- adr-init.sh --dir docs/decisions --first-title "Adopt ADRs"
- adr-init.sh --first-title "OAuth-only auth" --dry-run
- EOF
- }
- die_usage() { printf 'error: %s\n' "$1" >&2; echo >&2; usage >&2; exit "$EX_USAGE"; }
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --dir) [[ $# -ge 2 ]] || die_usage "--dir needs a value"; DIR="$2"; shift 2 ;;
- --first-title) [[ $# -ge 2 ]] || die_usage "--first-title needs a value"; FIRST_TITLE="$2"; shift 2 ;;
- --dry-run) DRY_RUN=1; shift ;;
- --force) FORCE=1; shift ;;
- -h|--help) usage; exit "$EX_OK" ;;
- -*) die_usage "unknown flag: $1" ;;
- *) die_usage "unexpected positional argument: $1" ;;
- esac
- done
- [[ -f "$ADR_NEW" ]] || { printf 'error: adr-new.sh not found beside this script: %s\n' "$ADR_NEW" >&2; exit 1; }
- # ── precondition: refuse a populated dir unless --force ─────────────────────
- if [[ -d "$DIR" ]]; then
- shopt -s nullglob
- existing=("$DIR"/ADR-*.md)
- shopt -u nullglob
- if [[ ${#existing[@]} -gt 0 && "$FORCE" -ne 1 ]]; then
- printf 'error: %s already contains %d ADR file(s); refusing to bootstrap (use --force to override)\n' \
- "$DIR" "${#existing[@]}" >&2
- exit "$EX_PRECOND"
- fi
- fi
- README_PATH="$DIR/README.md"
- readme_content() {
- cat <<EOF
- <!-- generated by adr-init.sh — do not hand-edit; the directory is the index -->
- # Architecture Decision Records
- This directory holds **Architecture Decision Records (ADRs)** — append-only
- project memory recording *why* the system is shaped the way it is. Each
- \`ADR-NNN-slug.md\` captures one decision: what was decided, the alternatives,
- and the consequences.
- ## When to write one
- Write an ADR when a change **constrains future options**, **seriously weighed
- alternatives**, or has **rationale the code can't show**. A reversible,
- low-stakes choice is a code comment, not an ADR.
- ## Format
- Numbered sequentially (\`highest + 1\`), never reused or reordered. Frontmatter
- carries \`status\`, \`date\`, \`supersedes\`, \`superseded-by\`, and \`touches:\`
- (the discovery surface). Body: Decision (one sentence) → Context → Alternatives
- considered → Consequences → See also.
- ## The directory IS the index
- Do not hand-maintain a numbered list here — it drifts. The authoritative list is
- the filesystem; run \`adr-index\` to regenerate a table view.
- EOF
- }
- # ── dry-run: describe, write nothing ────────────────────────────────────────
- if [[ "$DRY_RUN" -eq 1 ]]; then
- [[ -d "$DIR" ]] || printf 'would create directory: %s\n' "$DIR"
- printf 'would scaffold: %s\n' "$DIR/ADR-001-<slug-from-title>.md"
- printf 'would write: %s\n' "$README_PATH"
- {
- term_panel_open adr "adr ${TERM_DOT} init (dry-run)" "$DIR"
- term_panel_vert
- [[ -d "$DIR" ]] || term_status_row skip "would create dir" "$DIR"
- term_status_row skip "would scaffold ADR-001" "title: $FIRST_TITLE"
- term_status_row skip "would write README.md" ""
- term_panel_vert
- term_panel_close "nothing written" ""
- } >&2
- exit "$EX_OK"
- fi
- # Bootstrap outcome rows, collected then framed as one status panel on stderr.
- init_rows=() # "state\tlabel\tvalue"
- # ── create the directory ────────────────────────────────────────────────────
- if [[ ! -d "$DIR" ]]; then
- mkdir -p "$DIR" || { printf 'error: failed to create %s\n' "$DIR" >&2; exit 1; }
- printf '%s\n' "$DIR"
- init_rows+=("ok"$'\t'"created dir"$'\t'"$DIR")
- fi
- # ── scaffold ADR-001 via adr-new.sh (shares the canonical template) ─────────
- # Force number 001 so a --force re-run into a dir with higher numbers still
- # bootstraps the first record; adr-new's own guard refuses to clobber.
- new_out="$(bash "$ADR_NEW" --dir "$DIR" --number 001 --title "$FIRST_TITLE" 2>/dev/null)"
- new_rc=$?
- if [[ "$new_rc" -eq 0 ]]; then
- printf '%s\n' "$new_out"
- init_rows+=("ok"$'\t'"scaffolded $(basename "$new_out")"$'\t'"title: $FIRST_TITLE")
- elif [[ "$new_rc" -eq 5 ]]; then
- init_rows+=("skip"$'\t'"ADR-001 already present"$'\t'"left as-is")
- else
- printf 'error: adr-new.sh failed (exit %d) scaffolding ADR-001\n' "$new_rc" >&2
- exit 1
- fi
- # ── write the generated README (atomic; never clobber) ──────────────────────
- if [[ -e "$README_PATH" ]]; then
- init_rows+=("skip"$'\t'"README.md already present"$'\t'"left as-is")
- else
- tmp="$README_PATH.tmp.$$"
- readme_content > "$tmp" || { rm -f "$tmp"; printf 'error: failed to write %s\n' "$tmp" >&2; exit 1; }
- mv -f "$tmp" "$README_PATH" || { rm -f "$tmp"; printf 'error: failed to move into place: %s\n' "$README_PATH" >&2; exit 1; }
- printf '%s\n' "$README_PATH"
- init_rows+=("ok"$'\t'"wrote README.md"$'\t'"$README_PATH")
- fi
- {
- term_panel_open adr "adr ${TERM_DOT} init" "$DIR"
- term_panel_vert
- for row in "${init_rows[@]}"; do
- IFS=$'\t' read -r st lbl val <<<"$row"
- term_status_row "$st" "$lbl" "$val"
- done
- term_panel_vert
- term_panel_close "fill in ADR-001 ${TERM_DOT} then adr-lint before committing" ""
- } >&2
- exit "$EX_OK"
|