adr-init.sh 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env bash
  2. # Bootstrap an ADR directory in a repo adopting Architecture Decision Records cold.
  3. #
  4. # Usage: adr-init.sh [--dir DIR] [--first-title "TEXT"] [--dry-run] [--force]
  5. # Input: argv flags only (no stdin).
  6. # Output: stdout = paths created (or, under --dry-run, the actions it would take).
  7. # Data only.
  8. # Stderr: headers, reminders, warnings, errors.
  9. # Exit: 0 created (or dry-run rendered), 2 usage, 5 precondition
  10. # (dir already contains ADR-*.md and --force not given)
  11. #
  12. # Creates <dir> if missing, scaffolds a lint-clean ADR-001 (by invoking adr-new.sh
  13. # so it shares the canonical template), and writes a short generated <dir>/README.md
  14. # pointing at the directory-as-index discipline. Refuses to run in a directory that
  15. # already holds ADRs unless --force. Atomic writes; never clobbers existing files.
  16. #
  17. # Examples:
  18. # adr-init.sh
  19. # adr-init.sh --dir docs/decisions --first-title "Adopt ADRs"
  20. # adr-init.sh --first-title "OAuth-only auth" --dry-run
  21. set -uo pipefail
  22. readonly EX_OK=0 EX_USAGE=2 EX_PRECOND=5
  23. # Terminal design system (skills/_lib/term.sh). stdout = paths created (data); the
  24. # bootstrap summary frames on stderr, so detect color on fd 2. Degrade to plain
  25. # stderr lines if the shared lib is unreachable.
  26. __lib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../_lib" 2>/dev/null && pwd || true)"
  27. if [ -n "${__lib:-}" ] && [ -f "$__lib/term.sh" ]; then . "$__lib/term.sh"; term_init 2
  28. else
  29. term_panel_open() { :; }; term_panel_close() { :; }; term_panel_vert() { :; }
  30. term_status_row() { shift; printf ' - %s %s\n' "$1" "${2:-}"; }
  31. term_color() { shift; printf '%s' "$*"; }; TERM_DOT="|"
  32. fi
  33. DIR="docs/adr"
  34. FIRST_TITLE="Record architecture decisions"
  35. DRY_RUN=0
  36. FORCE=0
  37. HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  38. ADR_NEW="$HERE/adr-new.sh"
  39. usage() {
  40. cat <<'EOF'
  41. adr-init.sh — bootstrap an ADR directory in a repo adopting ADRs cold.
  42. Usage:
  43. adr-init.sh [--dir DIR] [--first-title "TEXT"] [--dry-run] [--force]
  44. Options:
  45. --dir DIR ADR directory to create/populate (default: docs/adr)
  46. --first-title TEXT Title for the scaffolded ADR-001
  47. (default: "Record architecture decisions")
  48. --dry-run Print what would happen; write nothing.
  49. --force Proceed even if DIR already contains ADR-*.md.
  50. -h, --help Show this help and exit 0.
  51. Exit codes:
  52. 0 created (or dry-run) 2 usage 5 dir already has ADRs (without --force)
  53. Examples:
  54. adr-init.sh
  55. adr-init.sh --dir docs/decisions --first-title "Adopt ADRs"
  56. adr-init.sh --first-title "OAuth-only auth" --dry-run
  57. EOF
  58. }
  59. die_usage() { printf 'error: %s\n' "$1" >&2; echo >&2; usage >&2; exit "$EX_USAGE"; }
  60. while [[ $# -gt 0 ]]; do
  61. case "$1" in
  62. --dir) [[ $# -ge 2 ]] || die_usage "--dir needs a value"; DIR="$2"; shift 2 ;;
  63. --first-title) [[ $# -ge 2 ]] || die_usage "--first-title needs a value"; FIRST_TITLE="$2"; shift 2 ;;
  64. --dry-run) DRY_RUN=1; shift ;;
  65. --force) FORCE=1; shift ;;
  66. -h|--help) usage; exit "$EX_OK" ;;
  67. -*) die_usage "unknown flag: $1" ;;
  68. *) die_usage "unexpected positional argument: $1" ;;
  69. esac
  70. done
  71. [[ -f "$ADR_NEW" ]] || { printf 'error: adr-new.sh not found beside this script: %s\n' "$ADR_NEW" >&2; exit 1; }
  72. # ── precondition: refuse a populated dir unless --force ─────────────────────
  73. if [[ -d "$DIR" ]]; then
  74. shopt -s nullglob
  75. existing=("$DIR"/ADR-*.md)
  76. shopt -u nullglob
  77. if [[ ${#existing[@]} -gt 0 && "$FORCE" -ne 1 ]]; then
  78. printf 'error: %s already contains %d ADR file(s); refusing to bootstrap (use --force to override)\n' \
  79. "$DIR" "${#existing[@]}" >&2
  80. exit "$EX_PRECOND"
  81. fi
  82. fi
  83. README_PATH="$DIR/README.md"
  84. readme_content() {
  85. cat <<EOF
  86. <!-- generated by adr-init.sh — do not hand-edit; the directory is the index -->
  87. # Architecture Decision Records
  88. This directory holds **Architecture Decision Records (ADRs)** — append-only
  89. project memory recording *why* the system is shaped the way it is. Each
  90. \`ADR-NNN-slug.md\` captures one decision: what was decided, the alternatives,
  91. and the consequences.
  92. ## When to write one
  93. Write an ADR when a change **constrains future options**, **seriously weighed
  94. alternatives**, or has **rationale the code can't show**. A reversible,
  95. low-stakes choice is a code comment, not an ADR.
  96. ## Format
  97. Numbered sequentially (\`highest + 1\`), never reused or reordered. Frontmatter
  98. carries \`status\`, \`date\`, \`supersedes\`, \`superseded-by\`, and \`touches:\`
  99. (the discovery surface). Body: Decision (one sentence) → Context → Alternatives
  100. considered → Consequences → See also.
  101. ## The directory IS the index
  102. Do not hand-maintain a numbered list here — it drifts. The authoritative list is
  103. the filesystem; run \`adr-index\` to regenerate a table view.
  104. EOF
  105. }
  106. # ── dry-run: describe, write nothing ────────────────────────────────────────
  107. if [[ "$DRY_RUN" -eq 1 ]]; then
  108. [[ -d "$DIR" ]] || printf 'would create directory: %s\n' "$DIR"
  109. printf 'would scaffold: %s\n' "$DIR/ADR-001-<slug-from-title>.md"
  110. printf 'would write: %s\n' "$README_PATH"
  111. {
  112. term_panel_open adr "adr ${TERM_DOT} init (dry-run)" "$DIR"
  113. term_panel_vert
  114. [[ -d "$DIR" ]] || term_status_row skip "would create dir" "$DIR"
  115. term_status_row skip "would scaffold ADR-001" "title: $FIRST_TITLE"
  116. term_status_row skip "would write README.md" ""
  117. term_panel_vert
  118. term_panel_close "nothing written" ""
  119. } >&2
  120. exit "$EX_OK"
  121. fi
  122. # Bootstrap outcome rows, collected then framed as one status panel on stderr.
  123. init_rows=() # "state\tlabel\tvalue"
  124. # ── create the directory ────────────────────────────────────────────────────
  125. if [[ ! -d "$DIR" ]]; then
  126. mkdir -p "$DIR" || { printf 'error: failed to create %s\n' "$DIR" >&2; exit 1; }
  127. printf '%s\n' "$DIR"
  128. init_rows+=("ok"$'\t'"created dir"$'\t'"$DIR")
  129. fi
  130. # ── scaffold ADR-001 via adr-new.sh (shares the canonical template) ─────────
  131. # Force number 001 so a --force re-run into a dir with higher numbers still
  132. # bootstraps the first record; adr-new's own guard refuses to clobber.
  133. new_out="$(bash "$ADR_NEW" --dir "$DIR" --number 001 --title "$FIRST_TITLE" 2>/dev/null)"
  134. new_rc=$?
  135. if [[ "$new_rc" -eq 0 ]]; then
  136. printf '%s\n' "$new_out"
  137. init_rows+=("ok"$'\t'"scaffolded $(basename "$new_out")"$'\t'"title: $FIRST_TITLE")
  138. elif [[ "$new_rc" -eq 5 ]]; then
  139. init_rows+=("skip"$'\t'"ADR-001 already present"$'\t'"left as-is")
  140. else
  141. printf 'error: adr-new.sh failed (exit %d) scaffolding ADR-001\n' "$new_rc" >&2
  142. exit 1
  143. fi
  144. # ── write the generated README (atomic; never clobber) ──────────────────────
  145. if [[ -e "$README_PATH" ]]; then
  146. init_rows+=("skip"$'\t'"README.md already present"$'\t'"left as-is")
  147. else
  148. tmp="$README_PATH.tmp.$$"
  149. readme_content > "$tmp" || { rm -f "$tmp"; printf 'error: failed to write %s\n' "$tmp" >&2; exit 1; }
  150. mv -f "$tmp" "$README_PATH" || { rm -f "$tmp"; printf 'error: failed to move into place: %s\n' "$README_PATH" >&2; exit 1; }
  151. printf '%s\n' "$README_PATH"
  152. init_rows+=("ok"$'\t'"wrote README.md"$'\t'"$README_PATH")
  153. fi
  154. {
  155. term_panel_open adr "adr ${TERM_DOT} init" "$DIR"
  156. term_panel_vert
  157. for row in "${init_rows[@]}"; do
  158. IFS=$'\t' read -r st lbl val <<<"$row"
  159. term_status_row "$st" "$lbl" "$val"
  160. done
  161. term_panel_vert
  162. term_panel_close "fill in ADR-001 ${TERM_DOT} then adr-lint before committing" ""
  163. } >&2
  164. exit "$EX_OK"