424 lines
12 KiB
Bash
424 lines
12 KiB
Bash
#!/usr/bin/env sh
|
||
set -eu
|
||
|
||
# Sync standards snapshot to project root.
|
||
# - Copies <snapshot>/rulesets/<AGENTS_NS> -> <project-root>/.agents/<AGENTS_NS>
|
||
# - Updates <project-root>/.gitattributes (append missing rules by default)
|
||
# Existing targets are backed up before overwrite.
|
||
#
|
||
# Multi rulesets:
|
||
# sh .../sync_standards.sh tsl cpp
|
||
# 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
|
||
sh scripts/sync_standards.sh tsl cpp
|
||
sh scripts/sync_standards.sh -langs tsl,cpp
|
||
|
||
Options:
|
||
-langs L1,L2 Comma/space-separated list of languages.
|
||
-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
|
||
|
||
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 <standards-url> <branch> --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
|
||
langs=""
|
||
if [ "${1:-}" = "-langs" ]; then
|
||
langs="${2:-}"
|
||
shift 2
|
||
fi
|
||
if [ -z "${langs:-}" ] && [ "$#" -gt 0 ]; then
|
||
langs="$*"
|
||
fi
|
||
if [ -z "${langs:-}" ] && [ "$#" -eq 0 ] && [ -z "${AGENTS_NS:-}" ]; then
|
||
auto_langs=""
|
||
if [ -d "$ROOT/.agents" ]; then
|
||
for dir in "$ROOT/.agents"/*; do
|
||
[ -d "$dir" ] || continue
|
||
ns="$(basename "$dir")"
|
||
if [ -d "$AGENTS_SRC_ROOT/$ns" ]; then
|
||
auto_langs="${auto_langs:+$auto_langs }$ns"
|
||
fi
|
||
done
|
||
fi
|
||
if [ -n "$auto_langs" ]; then
|
||
langs="$auto_langs"
|
||
fi
|
||
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"
|
||
else
|
||
SYNC_STANDARDS_INNER=1 AGENTS_NS="$ns" SYNC_GITATTR_MODE=skip sh "$0"
|
||
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 <snapshot>/.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="<!-- playbook:agents:start -->"
|
||
AGENTS_BLOCK_END="<!-- playbook:agents: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" "$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."
|