324 lines
9.3 KiB
Bash
324 lines
9.3 KiB
Bash
#!/usr/bin/env sh
|
|
set -eu
|
|
|
|
# Sync project templates to target project.
|
|
# - Copies templates/memory-bank/ -> <project-root>/memory-bank/
|
|
# - Copies templates/prompts/ -> <project-root>/docs/prompts/
|
|
# - Copies templates/AGENTS.template.md -> <project-root>/AGENTS.md
|
|
# - Copies templates/AGENT_RULES.template.md -> <project-root>/AGENT_RULES.md
|
|
# Existing targets are backed up before overwrite.
|
|
#
|
|
# Usage:
|
|
# sh scripts/sync_templates.sh # sync to current git root
|
|
# sh scripts/sync_templates.sh -project-root /path/to/project
|
|
# sh scripts/sync_templates.sh -project-root /path/to/project -project-name "MyProject" -date "2026-01-20"
|
|
#
|
|
# Options:
|
|
# -project-root PATH Target project root (default: git root)
|
|
# -project-name NAME Replace {{PROJECT_NAME}} placeholder
|
|
# -date DATE Replace {{DATE}} placeholder (default: today)
|
|
# -no-backup Skip backup of existing files
|
|
# -force Overwrite without prompting
|
|
# -full Append full framework (规则优先级 + 新会话开始时) to existing AGENTS.md
|
|
|
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)"
|
|
SRC="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)"
|
|
|
|
# Defaults
|
|
PROJECT_NAME=""
|
|
SYNC_DATE="$(date +%Y-%m-%d 2>/dev/null || echo "{{DATE}}")"
|
|
NO_BACKUP=0
|
|
FORCE=0
|
|
FULL=0
|
|
PROJECT_ROOT=""
|
|
|
|
# Parse arguments
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
-project-root)
|
|
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
|
|
echo "ERROR: -project-root requires a path." >&2
|
|
exit 1
|
|
fi
|
|
PROJECT_ROOT="$2"
|
|
shift 2
|
|
;;
|
|
-project-name)
|
|
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
|
|
echo "ERROR: -project-name requires a value." >&2
|
|
exit 1
|
|
fi
|
|
PROJECT_NAME="$2"
|
|
shift 2
|
|
;;
|
|
-date)
|
|
if [ $# -lt 2 ] || [ -z "${2:-}" ]; then
|
|
echo "ERROR: -date requires a value." >&2
|
|
exit 1
|
|
fi
|
|
SYNC_DATE="$2"
|
|
shift 2
|
|
;;
|
|
-no-backup)
|
|
NO_BACKUP=1
|
|
shift
|
|
;;
|
|
-force)
|
|
FORCE=1
|
|
shift
|
|
;;
|
|
-full)
|
|
FULL=1
|
|
shift
|
|
;;
|
|
-h|-help)
|
|
cat <<'EOF'
|
|
Usage:
|
|
sh scripts/sync_templates.sh [options]
|
|
sh scripts/sync_templates.sh -project-root /path/to/project
|
|
|
|
Options:
|
|
-project-root PATH Target project root (default: git root)
|
|
-project-name NAME Replace {{PROJECT_NAME}} placeholder
|
|
-date DATE Replace {{DATE}} placeholder (default: today)
|
|
-no-backup Skip backup of existing files
|
|
-force Overwrite without prompting
|
|
-full Append full framework (规则优先级 + 新会话开始时) to existing AGENTS.md
|
|
-h, -help Show this help
|
|
|
|
Examples:
|
|
sh scripts/sync_templates.sh
|
|
sh scripts/sync_templates.sh -project-root /path/to/project
|
|
sh scripts/sync_templates.sh -project-root /path/to/project -full
|
|
EOF
|
|
exit 0
|
|
;;
|
|
-*)
|
|
echo "ERROR: Unknown option: $1" >&2
|
|
exit 1
|
|
;;
|
|
*)
|
|
echo "ERROR: positional args are not supported; use -project-root." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Determine project root
|
|
if [ -z "$PROJECT_ROOT" ]; then
|
|
PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
fi
|
|
PROJECT_ROOT="$(CDPATH= cd -- "$PROJECT_ROOT" && pwd -P)"
|
|
|
|
# Source directories
|
|
TEMPLATES_DIR="$SRC/templates"
|
|
MEMORY_BANK_SRC="$TEMPLATES_DIR/memory-bank"
|
|
PROMPTS_SRC="$TEMPLATES_DIR/prompts"
|
|
AGENTS_SRC="$TEMPLATES_DIR/AGENTS.template.md"
|
|
AGENT_RULES_SRC="$TEMPLATES_DIR/AGENT_RULES.template.md"
|
|
|
|
# Check source exists
|
|
if [ ! -d "$TEMPLATES_DIR" ]; then
|
|
echo "ERROR: Templates directory not found: $TEMPLATES_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Skip if source equals destination (running from playbook repo itself)
|
|
if [ "$SRC" = "$PROJECT_ROOT" ]; then
|
|
echo "Skip: playbook root equals project root."
|
|
echo "Done."
|
|
exit 0
|
|
fi
|
|
|
|
timestamp="$(date +%Y%m%d%H%M%S 2>/dev/null || echo bak)"
|
|
|
|
# Function: backup file/directory
|
|
backup_if_exists() {
|
|
target="$1"
|
|
if [ -e "$target" ] && [ "$NO_BACKUP" -eq 0 ]; then
|
|
backup="${target}.bak.$timestamp"
|
|
mv "$target" "$backup"
|
|
echo "Backed up: $(basename "$target") -> $(basename "$backup")"
|
|
fi
|
|
}
|
|
|
|
escape_sed_replacement() {
|
|
printf '%s' "$1" | sed 's/[&/|\\]/\\&/g'
|
|
}
|
|
|
|
# Function: replace placeholders in file
|
|
replace_placeholders() {
|
|
file="$1"
|
|
[ -f "$file" ] || return 0
|
|
|
|
tmp="$(mktemp 2>/dev/null || echo "$file.tmp.$timestamp")"
|
|
date_repl="$(escape_sed_replacement "$SYNC_DATE")"
|
|
if [ -n "$PROJECT_NAME" ]; then
|
|
project_repl="$(escape_sed_replacement "$PROJECT_NAME")"
|
|
sed -e "s/{{PROJECT_NAME}}/$project_repl/g" -e "s/{{DATE}}/$date_repl/g" "$file" > "$tmp"
|
|
else
|
|
sed -e "s/{{DATE}}/$date_repl/g" "$file" > "$tmp"
|
|
fi
|
|
mv "$tmp" "$file"
|
|
}
|
|
|
|
# Function: replace placeholders in directory
|
|
replace_placeholders_dir() {
|
|
dir="$1"
|
|
[ -d "$dir" ] || return 0
|
|
|
|
find "$dir" -type f -name '*.md' -print | while IFS= read -r file; do
|
|
replace_placeholders "$file"
|
|
done
|
|
}
|
|
|
|
echo "Syncing templates to: $PROJECT_ROOT"
|
|
echo ""
|
|
|
|
# 1. Sync memory-bank/
|
|
if [ -d "$MEMORY_BANK_SRC" ]; then
|
|
MEMORY_BANK_DST="$PROJECT_ROOT/memory-bank"
|
|
|
|
if [ -e "$MEMORY_BANK_DST" ] && [ "$FORCE" -eq 0 ]; then
|
|
echo "memory-bank/ already exists. Use -force to overwrite."
|
|
else
|
|
backup_if_exists "$MEMORY_BANK_DST"
|
|
mkdir -p "$MEMORY_BANK_DST"
|
|
cp -R "$MEMORY_BANK_SRC"/* "$MEMORY_BANK_DST/" 2>/dev/null || true
|
|
|
|
# Rename .template.md to .md
|
|
for f in "$MEMORY_BANK_DST"/*.template.md; do
|
|
[ -f "$f" ] || continue
|
|
newname="$(echo "$f" | sed 's/\.template\.md$/.md/')"
|
|
mv "$f" "$newname"
|
|
done
|
|
|
|
replace_placeholders_dir "$MEMORY_BANK_DST"
|
|
echo "Synced: memory-bank/"
|
|
fi
|
|
else
|
|
echo "Skip: memory-bank/ templates not found"
|
|
fi
|
|
|
|
# 2. Sync docs/prompts/
|
|
if [ -d "$PROMPTS_SRC" ]; then
|
|
PROMPTS_DST="$PROJECT_ROOT/docs/prompts"
|
|
|
|
if [ -e "$PROMPTS_DST" ] && [ "$FORCE" -eq 0 ]; then
|
|
echo "docs/prompts/ already exists. Use -force to overwrite."
|
|
else
|
|
backup_if_exists "$PROMPTS_DST"
|
|
mkdir -p "$PROJECT_ROOT/docs"
|
|
mkdir -p "$PROMPTS_DST"
|
|
cp -R "$PROMPTS_SRC"/* "$PROMPTS_DST/" 2>/dev/null || true
|
|
|
|
# Rename .template.md to .md (recursive)
|
|
find "$PROMPTS_DST" -type f -name '*.template.md' -print | while IFS= read -r f; do
|
|
newname="$(echo "$f" | sed 's/\.template\.md$/.md/')"
|
|
mv "$f" "$newname"
|
|
done
|
|
|
|
replace_placeholders_dir "$PROMPTS_DST"
|
|
echo "Synced: docs/prompts/"
|
|
fi
|
|
else
|
|
echo "Skip: prompts/ templates not found"
|
|
fi
|
|
|
|
# 3. Sync AGENTS.md
|
|
# Choose markers based on -full flag
|
|
if [ "$FULL" -eq 1 ]; then
|
|
MARKER_START="<!-- playbook:framework:start -->"
|
|
MARKER_END="<!-- playbook:framework:end -->"
|
|
SECTION_NAME="framework"
|
|
else
|
|
MARKER_START="<!-- playbook:templates:start -->"
|
|
MARKER_END="<!-- playbook:templates:end -->"
|
|
SECTION_NAME="templates"
|
|
fi
|
|
|
|
if [ -f "$AGENTS_SRC" ]; then
|
|
AGENTS_DST="$PROJECT_ROOT/AGENTS.md"
|
|
|
|
if [ ! -e "$AGENTS_DST" ]; then
|
|
# AGENTS.md doesn't exist: create from full template
|
|
cp "$AGENTS_SRC" "$AGENTS_DST"
|
|
replace_placeholders "$AGENTS_DST"
|
|
echo "Created: AGENTS.md"
|
|
else
|
|
# AGENTS.md exists: update or append section
|
|
# Extract snippet from template
|
|
snippet_content="$(awk -v start="$MARKER_START" -v end="$MARKER_END" '
|
|
$0 ~ start { found=1 }
|
|
found { print }
|
|
$0 ~ end { found=0 }
|
|
' "$AGENTS_SRC")"
|
|
|
|
if [ -z "$snippet_content" ]; then
|
|
echo "Skip: markers not found in template"
|
|
elif grep -q "$MARKER_START" "$AGENTS_DST"; then
|
|
# Has markers: replace content between markers in place
|
|
snippet_tmp="$(mktemp 2>/dev/null || echo "$AGENTS_DST.snippet.$timestamp")"
|
|
printf "%s\n" "$snippet_content" > "$snippet_tmp"
|
|
tmp="$(mktemp 2>/dev/null || echo "$AGENTS_DST.tmp.$timestamp")"
|
|
awk -v start="$MARKER_START" -v end="$MARKER_END" -v snippet="$snippet_tmp" '
|
|
BEGIN {
|
|
while ((getline line < snippet) > 0) { block[++n] = line }
|
|
close(snippet)
|
|
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_DST" > "$tmp"
|
|
mv "$tmp" "$AGENTS_DST"
|
|
rm -f "$snippet_tmp"
|
|
replace_placeholders "$AGENTS_DST"
|
|
echo "Updated: AGENTS.md ($SECTION_NAME section)"
|
|
else
|
|
# No markers: append snippet at the end
|
|
echo "" >> "$AGENTS_DST"
|
|
echo "$snippet_content" >> "$AGENTS_DST"
|
|
replace_placeholders "$AGENTS_DST"
|
|
echo "Appended: AGENTS.md ($SECTION_NAME section)"
|
|
fi
|
|
fi
|
|
else
|
|
echo "Skip: AGENTS.template.md not found"
|
|
fi
|
|
|
|
# 4. Sync AGENT_RULES.md
|
|
if [ -f "$AGENT_RULES_SRC" ]; then
|
|
AGENT_RULES_DST="$PROJECT_ROOT/AGENT_RULES.md"
|
|
|
|
if [ -e "$AGENT_RULES_DST" ] && [ "$FORCE" -eq 0 ]; then
|
|
echo "AGENT_RULES.md already exists. Use -force to overwrite."
|
|
else
|
|
backup_if_exists "$AGENT_RULES_DST"
|
|
cp "$AGENT_RULES_SRC" "$AGENT_RULES_DST"
|
|
replace_placeholders "$AGENT_RULES_DST"
|
|
echo "Synced: AGENT_RULES.md"
|
|
fi
|
|
else
|
|
echo "Skip: AGENT_RULES.template.md not found"
|
|
fi
|
|
|
|
echo ""
|
|
echo "Done."
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Edit memory-bank/*.md to fill in project-specific content"
|
|
echo " 2. Replace remaining {{PLACEHOLDER}} values"
|
|
echo " 3. Run sync_standards.sh -langs <lang> to sync .agents/ rules"
|