learning_ai_common_plat/scripts/update-agent-docs.sh
saravanakumardb1 088a9cabd6 fix(agent-docs): update AI.dev guides + script UX after single-source migration
Followup audit of the single-source-of-truth agent-docs rollout. Several
AI.dev prompts and skills still taught agents the old 8-file pattern (which
would re-introduce drift) and the generator script emitted a misleading
summary in --no-commit mode.

AI.dev guides:
- Delete AI.dev/SKILLS/update-agent-docs.md — entire doc taught the old
  8-file pattern. Canonical reference is now
  .windsurf/workflows/repo_update-agent-docs.md.
- AI.dev/SKILLS/index.md + README.md: replace dangling 'Update Agent
  Documentation' link with pointers to agent-behavior-guidelines.md,
  agent-onboarding.md, and the workflow doc.
- AI.dev/SKILLS/scan-repo-context.md: remove instructions to read
  .windsurfrules / write .cursorrules. Point at the canonical behavior file.
- AI.dev/PROMPTS/new-product-scaffold.md: remove .windsurfrules and CLAUDE.md
  from the scaffold tree. Add deprecated-files callout + regeneration hint.
- AI.dev/PROMPTS/agents-md-sync.md: drop 'Step 4 update CLAUDE.md', point at
  the generator instead. Remove CLAUDE.md from `git add`.
- AI.dev/PROMPTS/ecosystem-audit.md: replace 'CLAUDE.md exists?' with
  'canonical-behavior-pointer block present? legacy files absent?'.

Script UX:
- scripts/update-agent-docs.sh: stop printing 'All repos already in sync'
  when --no-commit suppressed commits or --dry-run was used. Emit accurate
  per-mode summaries instead.
2026-05-23 12:06:28 -07:00

530 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# scripts/update-agent-docs.sh
#
# Maintains AI agent docs across the ByteLyst workspace with ZERO duplication.
#
# Architecture (locked 2026-05-23):
# Canonical source of behavior rules lives in ONE file:
# learning_ai_common_plat/AI.dev/SKILLS/agent-behavior-guidelines.md
#
# Per-repo files (managed by this script):
# AGENTS.md — repo-specific (hand-maintained).
# Script idempotently prepends a "Read first"
# pointer block pointing to the canonical file.
# .github/copilot-instructions.md — thin pointer (5 lines). Regenerated.
# .aider.conf.yml — Aider config (reads AGENTS.md). Regenerated.
# .editorconfig — editor config. Regenerated.
#
# Legacy files deleted by this script (they used to duplicate AGENTS.md content):
# .cursorrules, .windsurfrules, .clinerules, CLAUDE.md
#
# Usage:
# ./scripts/update-agent-docs.sh # apply changes + commit per repo
# ./scripts/update-agent-docs.sh --dry-run # show what would change, no writes
# ./scripts/update-agent-docs.sh --no-commit # write files but don't git commit
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPOS_TXT="${SCRIPT_DIR}/../.windsurf/workflows/repos.txt"
BASE_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
CANONICAL_REL_FROM_COMMON_PLAT="AI.dev/SKILLS/agent-behavior-guidelines.md"
DRY_RUN=false
NO_COMMIT=false
CHANGED_REPOS=()
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=true ;;
--no-commit) NO_COMMIT=true ;;
-h|--help)
sed -n '2,25p' "${BASH_SOURCE[0]}" | sed 's/^# \?//'
exit 0
;;
esac
done
# ── colours ────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERR]${NC} $*" >&2; }
header(){ echo -e "\n${CYAN}══ $* ══${NC}"; }
# ── per-repo metadata (only fields still needed) ───────────────────────────
# NAME — display name for headings
# ID — productId string
# STACK — short stack description
# BUILD_VFY — single command shown in copilot pointer & aider.conf
# LINT1-3 — optional lint commands for aider.conf
# AIDER_READ2 — second file aider should read alongside AGENTS.md
set_meta() {
NAME="" ID="" STACK="" BUILD_VFY=""
LINT1="" LINT2="" LINT3="" AIDER_READ2="README.md"
case "$1" in
learning_ai_common_plat)
NAME="@bytelyst Common Platform"; ID="(product-agnostic)"
STACK="TypeScript, ESM, pnpm workspace, Fastify 5, Vitest, Azure Cosmos DB"
BUILD_VFY="pnpm build && pnpm test && pnpm typecheck"
LINT1="pnpm build 2>&1 | tail -10"
LINT2="pnpm test 2>&1 | tail -10"
LINT3="pnpm typecheck 2>&1 | tail -10"
;;
learning_voice_ai_agent)
NAME="LysnrAI"; ID="lysnrai"
STACK="Python 3.12 (desktop/backend) + Next.js 16 (dashboards) + Fastify 5 (microservices in sibling repo)"
BUILD_VFY="python -m pytest tests/ -v --tb=short"
LINT1="cd user-dashboard-web && npx tsc --noEmit 2>&1 | tail -10"
LINT2="python -m pytest tests/ -x --tb=short 2>&1 | tail -10"
AIDER_READ2="README_MONO_REPO.md"
;;
learning_multimodal_memory_agents)
NAME="MindLyst"; ID="mindlyst"
STACK="KMP (shared) + SwiftUI (iOS) + Jetpack Compose (Android) + Next.js 16 (web)"
BUILD_VFY="./gradlew :shared:compileKotlinIosSimulatorArm64"
LINT1="cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64 2>&1 | tail -5"
LINT2="cd mindlyst-native/web && npx next build 2>&1 | tail -10"
AIDER_READ2="ARCHITECTURE.md"
;;
learning_ai_clock)
NAME="ChronoMind"; ID="chronomind"
STACK="Next.js 16 (web) + SwiftUI (iOS/Watch/Mac) + Jetpack Compose (Android) + Fastify 5 (backend)"
BUILD_VFY="cd web && npm test && npm run typecheck && npm run build"
LINT1="cd web && npm test 2>&1 | tail -10"
LINT2="cd web && npm run typecheck 2>&1 | tail -10"
AIDER_READ2="docs/PRD.md"
;;
learning_ai_fastgap)
NAME="NomGap"; ID="nomgap"
STACK="React Native (Expo SDK 55) + TypeScript + Fastify 5 (backend)"
BUILD_VFY="npm test && npm run typecheck"
LINT1="npm test 2>&1 | tail -10"
LINT2="npm run typecheck 2>&1 | tail -10"
AIDER_READ2="docs/PRD.md"
;;
learning_ai_jarvis_jr)
NAME="JarvisJr"; ID="jarvisjr"
STACK="SwiftUI (iOS/Watch/Mac) + Next.js 16 (web) + Jetpack Compose (Android) + Fastify 5 (backend)"
BUILD_VFY="cd web && npm test && npm run typecheck && npm run build"
LINT1="cd web && npm test 2>&1 | tail -10"
LINT2="cd web && npm run typecheck 2>&1 | tail -10"
AIDER_READ2="docs/ENHANCED_IDEA_v2.md"
;;
learning_ai_peakpulse)
NAME="PeakPulse"; ID="peakpulse"
STACK="SwiftUI (iOS 17+), SwiftData, MapKit, WeatherKit, ActivityKit, WidgetKit, App Intents"
BUILD_VFY="cd ios && xcodegen generate && xcodebuild -scheme PeakPulse -sdk iphonesimulator build"
LINT1="cd ios && xcodebuild -scheme PeakPulse -sdk iphonesimulator build 2>&1 | tail -20"
AIDER_READ2="docs/PRD.md"
;;
learning_ai_notes)
NAME="ByteLyst Agentic Notes"; ID="bytelyst-notes"
STACK="Docs + Fastify 5 backend scaffold with TypeScript ESM and @bytelyst/* shared packages"
BUILD_VFY="cd backend && npm test && npm run typecheck && npm run build"
LINT1="cd backend && npm test 2>&1 | tail -10"
LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
LINT3="cd backend && npm run build 2>&1 | tail -10"
AIDER_READ2="docs/ROADMAP.md"
;;
learning_ai_flowmonk)
NAME="FlowMonk"; ID="flowmonk"
STACK="Next.js 16 (web) + React Native/Expo (mobile) + Fastify 5 (backend)"
BUILD_VFY="cd backend && npm run typecheck && npm run build && cd ../web && npm run typecheck && npm run build && cd ../mobile && npm run typecheck"
LINT1="cd backend && npm run typecheck 2>&1 | tail -10"
LINT2="cd web && npm run typecheck 2>&1 | tail -10"
LINT3="cd mobile && npm run typecheck 2>&1 | tail -10"
AIDER_READ2="docs/ROADMAP.md"
;;
learning_ai_trails)
NAME="ActionTrail"; ID="actiontrail"
STACK="Next.js 16 (web) + Fastify 5 (backend) + TypeScript ESM"
BUILD_VFY="cd backend && npm test && npm run typecheck && cd ../sdk && npm test && npm run typecheck && cd ../web && npm run typecheck"
LINT1="cd backend && npm test 2>&1 | tail -10"
LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
LINT3="cd sdk && npm run typecheck 2>&1 | tail -10"
AIDER_READ2="docs/roadmap.md"
;;
learning_ai_local_memory_gpt)
NAME="Local Memory GPT"; ID="localmemgpt"
STACK="Fastify 5 + TypeScript ESM (backend) + Next.js 16 (web) + SQLite + Ollama"
BUILD_VFY="cd backend && npm test && npm run typecheck && npm run build && cd ../web && npm run typecheck && npm run build"
LINT1="cd backend && npm test 2>&1 | tail -10"
LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
LINT3="cd web && npm run typecheck 2>&1 | tail -10"
;;
learning_ai_smart_auth)
NAME="ByteLyst SmartAuth"; ID="smartauth"
STACK="Design/docs repo extending platform-service auth, shared auth packages, and native SDKs"
BUILD_VFY="cd ../learning_ai_common_plat && pnpm build && pnpm test && pnpm typecheck"
LINT1="cd ../learning_ai_common_plat && pnpm build 2>&1 | tail -10"
LINT2="cd ../learning_ai_common_plat && pnpm test 2>&1 | tail -10"
LINT3="cd ../learning_ai_common_plat && pnpm typecheck 2>&1 | tail -10"
;;
learning_ai_auth_app)
NAME="ByteLyst Auth"; ID="smartauth"
STACK="SwiftUI (iOS/Watch) + Jetpack Compose (Android) — no separate backend"
BUILD_VFY="cd ios && xcodegen generate && xcodebuild -scheme ByteLystAuth -sdk iphonesimulator build && cd ../android && ./gradlew :app:assembleDebug"
LINT1="cd ios && xcodegen generate >/dev/null 2>&1 && xcodebuild -scheme ByteLystAuth -sdk iphonesimulator build 2>&1 | tail -20"
LINT2="cd android && ./gradlew :app:assembleDebug 2>&1 | tail -20"
;;
learning_ai_efforise)
NAME="EffoRise"; ID="efforise"
STACK="Vite + React 19 SPA (client/) + Fastify 5 backend (backend/) + React Native/Expo (mobile/) + @bytelyst/* shared packages"
BUILD_VFY="cd backend && pnpm test && pnpm run typecheck && pnpm run build"
LINT1="cd backend && pnpm test 2>&1 | tail -10"
LINT2="cd backend && pnpm run typecheck 2>&1 | tail -10"
LINT3="cd backend && pnpm run build 2>&1 | tail -10"
;;
learning_ai_local_llms)
NAME="Local LLM Lab"; ID="localllmlab"
STACK="Next.js 16 (dashboard) + Ollama + @bytelyst/llm-router + Python (TTS)"
BUILD_VFY="cd dashboard && pnpm test && pnpm typecheck && pnpm build"
LINT1="cd dashboard && pnpm test 2>&1 | tail -10"
LINT2="cd dashboard && pnpm typecheck 2>&1 | tail -10"
LINT3="cd dashboard && pnpm build 2>&1 | tail -10"
;;
learning_ai_productivity_web)
NAME="Productivity Web"; ID="(internal)"
STACK="Next.js 15 + React 19 + TailwindCSS"
BUILD_VFY="npm run typecheck && npm run build"
LINT1="npm run typecheck 2>&1 | tail -10"
LINT2="npm run build 2>&1 | tail -10"
;;
learning_ai_talk2obsidian)
NAME="Talk2Obs"; ID="talk2obs"
STACK="Fastify 5 + TypeScript ESM (backend) + Vite + React 19 (web) + SQLite + Ollama + whisper-cpp"
BUILD_VFY="cd backend && npm test && npm run typecheck"
LINT1="cd backend && npm test 2>&1 | tail -10"
LINT2="cd backend && npm run typecheck 2>&1 | tail -10"
AIDER_READ2="docs/ROADMAP.md"
;;
learning_ai_mac_tooling)
NAME="Mac Tooling"; ID="(internal)"
STACK="Python (FastAPI backend) + Vite + React (dashboard) + SQLite + Swift (menu bar)"
BUILD_VFY="cd dashboard && npm run build"
LINT1="cd dashboard && npm run build 2>&1 | tail -10"
;;
oss/learning_ai_claw-code-oss)
NAME="Claw Code OSS"; ID="(upstream)"
STACK="Rust (workspace) + Python + TypeScript"
BUILD_VFY="cargo build --workspace && cargo test --workspace"
LINT1="cargo fmt --check 2>&1 | tail -10"
LINT2="cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10"
LINT3="cargo test --workspace 2>&1 | tail -10"
;;
oss/learning_ai_claw-cowork)
NAME="Claw Cowork"; ID="clawcowork"
STACK="Rust (workspace) + React/TypeScript (Tauri frontend) + Python (skills server)"
BUILD_VFY="cargo build --workspace && cargo test --workspace"
LINT1="cargo fmt --check 2>&1 | tail -10"
LINT2="cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -10"
LINT3="cargo test --workspace 2>&1 | tail -10"
AIDER_READ2="COWORK.md"
;;
*)
warn "Unknown repo: $1 — using minimal defaults"
NAME="$1"; ID="(unknown)"
STACK="(see AGENTS.md)"
BUILD_VFY="(see AGENTS.md)"
;;
esac
}
# Compute relative path from a repo to the canonical guidelines file.
# Repos at workspace root (depth 0) → ../learning_ai_common_plat/...
# Repos one level deep (e.g. oss/foo, depth 1) → ../../learning_ai_common_plat/...
# common_plat itself → AI.dev/SKILLS/...
canonical_path_for() {
local repo="$1"
if [[ "$repo" == "learning_ai_common_plat" ]]; then
echo "${CANONICAL_REL_FROM_COMMON_PLAT}"
return
fi
local slashes="${repo//[^\/]/}"
local depth=${#slashes}
local prefix=""
for ((i=0; i<=depth; i++)); do
prefix+="../"
done
echo "${prefix}learning_ai_common_plat/${CANONICAL_REL_FROM_COMMON_PLAT}"
}
# ── file generators ─────────────────────────────────────────────────────────
write_editorconfig() {
cat > "$1" << 'EOF'
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
EOF
}
write_aider_conf() {
local dest="$1"
local lint_block=""
for cmd in "${LINT1:-}" "${LINT2:-}" "${LINT3:-}"; do
[[ -z "$cmd" ]] && continue
lint_block+=" - '${cmd}'"$'\n'
done
local read_block=" - AGENTS.md"$'\n'
[[ -n "${AIDER_READ2:-}" ]] && read_block+=" - ${AIDER_READ2}"$'\n'
cat > "$dest" << EOF
# .aider.conf.yml — Aider Configuration for ${NAME}
# Auto-generated by learning_ai_common_plat/scripts/update-agent-docs.sh
# Hand-edit will be overwritten on next run.
read:
${read_block}
conventions: AGENTS.md
lint-cmd:
${lint_block}
auto-commits: false
EOF
}
write_copilot_pointer() {
local dest_dir="$1" canonical="$2"
mkdir -p "$dest_dir"
cat > "${dest_dir}/copilot-instructions.md" << EOF
<!-- AUTO-GENERATED by learning_ai_common_plat/scripts/update-agent-docs.sh -->
<!-- DO NOT EDIT. Edit the canonical sources instead. -->
# GitHub Copilot Instructions — ${NAME}
**Project:** ${NAME} — \`${ID}\`
**Stack:** ${STACK}
## Read these (in order) before suggesting code
1. **\`AGENTS.md\`** at the repo root — repo-specific layout, conventions,
build commands, file ownership.
2. **\`${canonical}\`** —
ecosystem-wide agent behavior rules (Karpathy + ByteLyst). Identical
across every repo in the workspace.
Both files together are the complete contract. This pointer file contains
no rules of its own to avoid duplication and drift.
## Quick verification command
\`\`\`bash
${BUILD_VFY}
\`\`\`
EOF
}
# Idempotently prepend a "Read first" pointer block to AGENTS.md.
# The block is delimited by HTML comment markers so we can re-find and
# replace it without touching the rest of AGENTS.md.
#
# Note: awk's -v does not preserve literal newlines reliably, so the
# replacement block is written to a temp file and slurped via getline.
prepend_agents_pointer() {
local agents_md="$1" canonical="$2"
[[ ! -f "$agents_md" ]] && return 0
local begin_marker="<!-- BEGIN: canonical-behavior-pointer (auto-managed) -->"
local end_marker="<!-- END: canonical-behavior-pointer -->"
local tmp_block
tmp_block="$(mktemp)"
cat > "$tmp_block" << EOF
${begin_marker}
> **Read first (ecosystem-wide agent behavior):**
> [\`${canonical}\`](${canonical})
>
> The link above is the single source of truth for agent behavior across every
> ByteLyst repo (Karpathy + ByteLyst rules: tests sacred, verify before done,
> no shared-infra hand-edits, no \`console.log\`/\`print\`, productId on every
> Cosmos doc, conventional commits, style preservation).
>
> The per-repo content below extends — never duplicates — the canonical rules.
${end_marker}
EOF
if grep -qF "$begin_marker" "$agents_md"; then
# Replace existing block in place.
awk -v begin="$begin_marker" -v endm="$end_marker" -v block_file="$tmp_block" '
BEGIN {
while ((getline line < block_file) > 0) {
new = (new == "" ? line : new "\n" line)
}
close(block_file)
in_block = 0
}
$0 == begin { print new; in_block = 1; next }
in_block && $0 == endm { in_block = 0; next }
!in_block { print }
' "$agents_md" > "${agents_md}.tmp" && mv "${agents_md}.tmp" "$agents_md"
else
# Insert after first H1 (line beginning with "# "). If no H1, prepend.
if grep -qE '^# ' "$agents_md"; then
awk -v block_file="$tmp_block" '
BEGIN {
while ((getline line < block_file) > 0) {
block = (block == "" ? line : block "\n" line)
}
close(block_file)
inserted = 0
}
{ print }
!inserted && /^# / { print ""; print block; inserted = 1 }
' "$agents_md" > "${agents_md}.tmp" && mv "${agents_md}.tmp" "$agents_md"
else
{ cat "$tmp_block"; echo ""; cat "$agents_md"; } > "${agents_md}.tmp" \
&& mv "${agents_md}.tmp" "$agents_md"
fi
fi
rm -f "$tmp_block"
}
# Delete legacy agent files (replaced by the canonical pointer architecture).
delete_legacy_files() {
local repo_dir="$1"
local deleted=()
for f in .cursorrules .windsurfrules .clinerules CLAUDE.md; do
local target="${repo_dir}/${f}"
[[ ! -e "$target" ]] && continue
if git -C "$repo_dir" ls-files --error-unmatch "$f" >/dev/null 2>&1; then
git -C "$repo_dir" rm -q "$f" 2>/dev/null || rm -f "$target"
else
rm -f "$target"
fi
deleted+=("$f")
done
if [[ ${#deleted[@]} -gt 0 ]]; then
ok "deleted legacy: ${deleted[*]}"
fi
}
# ── main loop ───────────────────────────────────────────────────────────────
if [[ ! -f "$REPOS_TXT" ]]; then
err "repos.txt not found at: ${REPOS_TXT}"
exit 1
fi
REPOS=()
while IFS= read -r line; do
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "${line// }" ]] && continue
REPOS+=("$line")
done < "$REPOS_TXT"
info "Base directory: ${BASE_DIR}"
info "Repos to process: ${#REPOS[@]}"
$DRY_RUN && warn "DRY RUN — no files will be written or committed"
$NO_COMMIT && info "NO-COMMIT mode — files will be written but not committed"
for REPO in "${REPOS[@]}"; do
header "$REPO"
REPO_DIR="${BASE_DIR}/${REPO}"
if [[ ! -d "$REPO_DIR" ]]; then
warn "Directory not found at ${REPO_DIR} — skipping"
continue
fi
set_meta "$REPO"
CANONICAL_PATH="$(canonical_path_for "$REPO")"
info "canonical pointer → ${CANONICAL_PATH}"
if [[ ! -f "${REPO_DIR}/AGENTS.md" ]]; then
warn "AGENTS.md not found in ${REPO} — skipping (bootstrap AGENTS.md manually first)"
continue
fi
if $DRY_RUN; then
info "Would: delete legacy files, rewrite copilot pointer, prepend AGENTS.md pointer"
info " regenerate .editorconfig and .aider.conf.yml"
continue
fi
# 1. Delete legacy files (.cursorrules, .windsurfrules, .clinerules, CLAUDE.md)
delete_legacy_files "$REPO_DIR"
# 2. Regenerate small config files
write_editorconfig "${REPO_DIR}/.editorconfig"
ok ".editorconfig"
write_aider_conf "${REPO_DIR}/.aider.conf.yml"
ok ".aider.conf.yml"
# 3. Rewrite copilot pointer (thin, no rules)
write_copilot_pointer "${REPO_DIR}/.github" "$CANONICAL_PATH"
ok ".github/copilot-instructions.md (pointer)"
# 4. Idempotently prepend canonical pointer to AGENTS.md
prepend_agents_pointer "${REPO_DIR}/AGENTS.md" "$CANONICAL_PATH"
ok "AGENTS.md (pointer block ensured)"
# 5. Commit if there are any changes in this repo
if $NO_COMMIT; then
info "Skipping commit (--no-commit)"
continue
fi
if git -C "$REPO_DIR" status --porcelain 2>/dev/null | grep -q .; then
# Stage only paths that currently exist (legacy file deletes are already
# staged by `git rm` inside delete_legacy_files()).
for f in .editorconfig .aider.conf.yml AGENTS.md .github/copilot-instructions.md; do
[[ -e "${REPO_DIR}/${f}" ]] && git -C "$REPO_DIR" add "$f" 2>/dev/null || true
done
if git -C "$REPO_DIR" commit -m "chore(docs): consolidate agent docs to single source of truth
- Delete legacy .cursorrules, .windsurfrules, .clinerules, CLAUDE.md
(content duplicated AGENTS.md; replaced by canonical pointer architecture)
- Rewrite .github/copilot-instructions.md as thin pointer to AGENTS.md
and the canonical agent-behavior-guidelines.md
- Prepend canonical 'Read first' pointer block to AGENTS.md
- Regenerate .editorconfig and .aider.conf.yml
Canonical source: learning_ai_common_plat/AI.dev/SKILLS/agent-behavior-guidelines.md
Managed by: learning_ai_common_plat/scripts/update-agent-docs.sh" \
--no-verify 2>/dev/null; then
ok "Committed changes in ${REPO}"
CHANGED_REPOS+=("$REPO")
else
warn "Detected changes in ${REPO}, but no commit was created"
fi
else
info "No changes in ${REPO}"
fi
done
# ── summary ──────────────────────────────────────────────────────────────────
echo ""
if [[ ${#CHANGED_REPOS[@]} -gt 0 ]]; then
ok "Updated and committed ${#CHANGED_REPOS[@]} repo(s):"
for r in "${CHANGED_REPOS[@]}"; do
echo "$r"
done
echo ""
info "Push when ready:"
for r in "${CHANGED_REPOS[@]}"; do
echo " git -C ${BASE_DIR}/${r} push"
done
else
if $NO_COMMIT; then
ok "Files regenerated. No commits made (--no-commit). Inspect changes with: git -C <repo> status"
elif $DRY_RUN; then
ok "Dry run complete. Re-run without --dry-run to apply."
else
ok "All repos already in sync — no changes."
fi
fi