#!/usr/bin/env sh set -eu # Sync standards snapshot to project root. # - Copies /rulesets/ -> /.agents/ # - Updates /.gitattributes (append missing rules by default) # Existing targets are backed up before overwrite. # # Multi rulesets: # sh .../sync_standards.sh -langs tsl,cpp # Notes: # - When syncing multiple rulesets, .gitattributes is synced only once (first ruleset). SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" SRC="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)" if [ -n "${SYNC_ROOT:-}" ]; then ROOT="$SYNC_ROOT" else ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)" fi ROOT="$(CDPATH= cd -- "$ROOT" && pwd -P)" usage() { cat <<'EOF' >&2 Usage: sh scripts/sync_standards.sh -langs tsl sh scripts/sync_standards.sh -langs tsl,cpp Options: -langs L1,L2 Comma/space-separated list of languages (required). -h, -help Show this help. Env: SYNC_ROOT Target project root (default: git root). AGENTS_NS Single ruleset name (default: tsl). SYNC_GITATTR_MODE append|overwrite|block|skip (default: append). EOF } if [ "${1:-}" = "-h" ] || [ "${1:-}" = "-help" ]; then usage exit 0 fi langs="" while [ $# -gt 0 ]; do case "$1" in -langs) if [ $# -lt 2 ] || [ -z "${2:-}" ]; then echo "ERROR: -langs requires a value." >&2 usage exit 1 fi langs="$2" shift 2 ;; -*) echo "ERROR: Unknown option: $1" >&2 usage exit 1 ;; *) echo "ERROR: positional args are not supported; use -langs." >&2 usage exit 1 ;; esac done AGENTS_SRC_ROOT="$SRC/rulesets" GITATTR_SRC="$SRC/.gitattributes" if [ ! -d "$AGENTS_SRC_ROOT" ]; then echo "ERROR: Standards snapshot not found at $AGENTS_SRC_ROOT" >&2 echo "Run: git subtree add --prefix docs/standards/playbook --squash" >&2 exit 1 fi timestamp="$(date +%Y%m%d%H%M%S 2>/dev/null || echo bak)" if [ "$SRC" = "$ROOT" ]; then echo "Skip: snapshot root equals project root." echo "Done." exit 0 fi # Parse multi rulesets only on the outer invocation. if [ "${SYNC_STANDARDS_INNER:-}" != "1" ]; then if [ -z "${langs:-}" ] && [ -z "${AGENTS_NS:-}" ]; then echo "ERROR: -langs is required." >&2 usage exit 1 fi if [ -n "${langs:-}" ]; then sync_mode_first="${SYNC_GITATTR_MODE:-append}" first=1 old_ifs="${IFS}" IFS=', ' set -- $langs IFS="${old_ifs}" for ns in "$@"; do [ -n "$ns" ] || continue if [ "$first" -eq 1 ]; then first=0 SYNC_STANDARDS_INNER=1 AGENTS_NS="$ns" SYNC_GITATTR_MODE="$sync_mode_first" sh "$0" -langs "$ns" else SYNC_STANDARDS_INNER=1 AGENTS_NS="$ns" SYNC_GITATTR_MODE=skip sh "$0" -langs "$ns" fi done exit 0 fi fi : "${AGENTS_NS:=tsl}" case "$AGENTS_NS" in ""|*/*|*\\*|*..*) echo "ERROR: invalid AGENTS_NS=$AGENTS_NS" >&2 exit 1 ;; esac AGENTS_SRC="$AGENTS_SRC_ROOT/$AGENTS_NS" if [ ! -d "$AGENTS_SRC" ]; then # Backward-compatible fallback: older snapshots used /.agents/* directly. if [ -f "$AGENTS_SRC_ROOT/index.md" ] && [ -f "$AGENTS_SRC_ROOT/auth.md" ]; then AGENTS_SRC="$AGENTS_SRC_ROOT" else echo "ERROR: agents ruleset not found: $AGENTS_SRC" >&2 echo "Hint: set AGENTS_NS to one of the subdirs under $AGENTS_SRC_ROOT (e.g. tsl/cpp)." >&2 exit 1 fi fi AGENTS_ROOT="$ROOT/.agents" AGENTS_DST="$AGENTS_ROOT/$AGENTS_NS" mkdir -p "$AGENTS_ROOT" if [ -e "$AGENTS_DST" ]; then mv "$AGENTS_DST" "$AGENTS_ROOT/$AGENTS_NS.bak.$timestamp" echo "Backed up existing $AGENTS_NS agents -> $AGENTS_NS.bak.$timestamp" fi cp -R "$AGENTS_SRC" "$AGENTS_DST" echo "Synced .agents/$AGENTS_NS from standards." # Rewrite docs/* references to the snapshot docs path. REL_SNAPSHOT="" case "$SRC" in "$ROOT"/*) REL_SNAPSHOT="${SRC#$ROOT/}" ;; esac if [ -n "$REL_SNAPSHOT" ]; then DOCS_PREFIX="$REL_SNAPSHOT/docs" for md in "$AGENTS_DST"/*.md; do [ -f "$md" ] || continue tmp="$(mktemp 2>/dev/null || echo "$AGENTS_DST/.rewrite.$(basename "$md").$timestamp")" sed \ -e 's#`docs/tsl/#`'"$DOCS_PREFIX"'/tsl/#g' \ -e 's#`docs/cpp/#`'"$DOCS_PREFIX"'/cpp/#g' \ -e 's#`docs/python/#`'"$DOCS_PREFIX"'/python/#g' \ -e 's#`docs/markdown/#`'"$DOCS_PREFIX"'/markdown/#g' \ -e 's#`docs/common/#`'"$DOCS_PREFIX"'/common/#g' \ "$md" >"$tmp" mv "$tmp" "$md" done fi AGENTS_INDEX="$AGENTS_ROOT/index.md" if [ ! -f "$AGENTS_INDEX" ]; then cat >"$AGENTS_INDEX" <<'EOF' # .agents(多语言) 本目录用于存放仓库级/语言级的代理规则集。 建议约定: - `.agents/tsl/`:TSL 相关规则集(由 `sync_standards.*` 同步;适用于 `.tsl`/`.tsf`) - `.agents/cpp/`:C++ 相关规则集(由 `sync_standards.*` 同步;适用于 C++23/Modules) - `.agents/python/`:Python 相关规则集(由 `sync_standards.*` 同步) - `.agents/markdown/`:Markdown 相关规则集(仅代码格式化) 规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。 入口建议从: - `.agents/tsl/index.md`(TSL 规则集入口) - `.agents/cpp/index.md`(C++ 规则集入口) - `.agents/markdown/index.md`(Markdown 规则集入口) - `docs/standards/playbook/docs/`(人类开发规范快照:`tsl/`、`cpp/`、`python/`、`common/`) EOF echo "Created .agents/index.md" fi AGENTS_MD="$ROOT/AGENTS.md" AGENTS_BLOCK_START="" AGENTS_BLOCK_END="" AGENTS_BLOCK_TMP="$(mktemp 2>/dev/null || echo "$ROOT/.agents_block.$timestamp")" agents_langs="" if [ -d "$AGENTS_ROOT" ]; then for dir in "$AGENTS_ROOT"/*; do [ -d "$dir" ] || continue name="$(basename "$dir")" case "$name" in ""|.*|*.bak.*) continue ;; esac if [ -f "$dir/index.md" ]; then agents_langs="${agents_langs:+$agents_langs }$name" fi done fi if [ -z "$agents_langs" ]; then agents_langs="$AGENTS_NS" fi langs_line="" for name in $agents_langs; do entry='`.agents/'"$name"'/index.md`' if [ -z "$langs_line" ]; then langs_line="$entry" else langs_line="$langs_line、$entry" fi done { printf "%s\n\n" "$AGENTS_BLOCK_START" printf "%s\n\n" '请以 `.agents/` 下的规则为准:' printf "%s\n" '- 入口:`.agents/index.md`' if [ -n "$langs_line" ]; then printf "%s\n" "- 语言规则:$langs_line" else printf "%s\n" "- 语言规则:" fi printf "%s\n" "$AGENTS_BLOCK_END" } >"$AGENTS_BLOCK_TMP" if [ ! -f "$AGENTS_MD" ]; then { printf "%s\n\n" "# Agent Instructions" cat "$AGENTS_BLOCK_TMP" } >"$AGENTS_MD" echo "Created AGENTS.md" else if grep -Fq "$AGENTS_BLOCK_START" "$AGENTS_MD"; then tmp="$(mktemp 2>/dev/null || echo "$ROOT/.agents_md.$timestamp")" awk -v start="$AGENTS_BLOCK_START" -v end="$AGENTS_BLOCK_END" -v block_file="$AGENTS_BLOCK_TMP" ' BEGIN { while ((getline line < block_file) > 0) { block[++n] = line } close(block_file) inblock=0 replaced=0 } { if (!replaced && $0 == start) { for (i=1; i<=n; i++) print block[i] inblock=1 replaced=1 next } if (inblock) { if ($0 == end) { inblock=0 } next } print } ' "$AGENTS_MD" >"$tmp" mv "$tmp" "$AGENTS_MD" echo "Updated AGENTS.md (playbook block)." else if grep -Fq ".agents/index.md" "$AGENTS_MD"; then echo "Skip: AGENTS.md already references .agents/index.md" else printf "\n" >>"$AGENTS_MD" cat "$AGENTS_BLOCK_TMP" >>"$AGENTS_MD" printf "\n" >>"$AGENTS_MD" echo "Appended playbook block to AGENTS.md" fi fi fi rm -f "$AGENTS_BLOCK_TMP" echo "Synced agents ruleset to $AGENTS_DST." GITATTR_DST="$ROOT/.gitattributes" if [ -f "$GITATTR_SRC" ]; then : "${SYNC_GITATTR_MODE:=append}" case "$SYNC_GITATTR_MODE" in skip) echo "Skip: .gitattributes sync (SYNC_GITATTR_MODE=skip)." ;; overwrite) if [ "$(CDPATH= cd -- "$(dirname -- "$GITATTR_SRC")" && pwd -P)/$(basename -- "$GITATTR_SRC")" = "$GITATTR_DST" ]; then echo "Skip: .gitattributes source equals destination." else if [ -e "$GITATTR_DST" ]; then mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" fi cp "$GITATTR_SRC" "$GITATTR_DST" echo "Synced .gitattributes from standards (overwrite)." fi ;; append) if [ "$(CDPATH= cd -- "$(dirname -- "$GITATTR_SRC")" && pwd -P)/$(basename -- "$GITATTR_SRC")" = "$GITATTR_DST" ]; then echo "Skip: .gitattributes source equals destination." else missing_tmp="$(mktemp 2>/dev/null || echo "$ROOT/.gitattributes.missing.$timestamp")" if [ -f "$GITATTR_DST" ]; then awk ' function norm(line) { gsub(/^[ \t]+|[ \t]+$/, "", line) return line } FNR==NR { line=norm($0) if (line == "" || line ~ /^#/) next seen[line]=1 next } { line=norm($0) if (line == "" || line ~ /^#/) next if (!seen[line] && !out[line]++) print line } ' "$GITATTR_DST" "$GITATTR_SRC" >"$missing_tmp" else awk ' function norm(line) { gsub(/^[ \t]+|[ \t]+$/, "", line) return line } { line=norm($0) if (line == "" || line ~ /^#/) next if (!out[line]++) print line } ' "$GITATTR_SRC" >"$missing_tmp" fi if [ ! -s "$missing_tmp" ]; then rm -f "$missing_tmp" echo "No missing .gitattributes rules to append." echo "Done." exit 0 fi if [ -e "$GITATTR_DST" ]; then mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" fi source_note="$GITATTR_SRC" case "$GITATTR_SRC" in "$ROOT"/*) source_note="${GITATTR_SRC#$ROOT/}" ;; esac header="# Added from playbook .gitattributes (source: $source_note)" { if [ -f "$ROOT/.gitattributes.bak.$timestamp" ]; then cat "$ROOT/.gitattributes.bak.$timestamp" if [ -s "$ROOT/.gitattributes.bak.$timestamp" ]; then printf "\n" fi fi printf "%s\n" "$header" cat "$missing_tmp" } >"$GITATTR_DST" rm -f "$missing_tmp" echo "Appended missing .gitattributes rules from standards." fi ;; block) begin="# BEGIN playbook .gitattributes" end="# END playbook .gitattributes" begin_old="# BEGIN tsl-playbook .gitattributes" end_old="# END tsl-playbook .gitattributes" if [ -e "$GITATTR_DST" ]; then mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" fi tmp="${GITATTR_DST}.tmp.${timestamp}" if [ -f "$ROOT/.gitattributes.bak.$timestamp" ]; then src_dst="$ROOT/.gitattributes.bak.$timestamp" else src_dst="" fi if [ -n "$src_dst" ]; then awk -v begin="$begin" -v end="$end" -v begin_old="$begin_old" -v end_old="$end_old" -v src="$GITATTR_SRC" ' function emit_src() { print begin while ((getline line < src) > 0) print line close(src) print end } BEGIN { in_block=0; done=0 } $0 == begin || $0 == begin_old { in_block=1; if (!done) { emit_src(); done=1 } ; next } $0 == end || $0 == end_old { in_block=0; next } !in_block { print } END { if (!done) { if (NR > 0) print "" emit_src() } } ' "$src_dst" >"$tmp" else { printf "%s\n" "$begin" cat "$GITATTR_SRC" printf "\n%s\n" "$end" } >"$tmp" fi mv "$tmp" "$GITATTR_DST" echo "Updated .gitattributes from standards (managed block)." ;; *) echo "ERROR: invalid SYNC_GITATTR_MODE=$SYNC_GITATTR_MODE (use block|overwrite|append|skip)" >&2 exit 1 ;; esac fi echo "Done."