#!/usr/bin/env bash # Changelog sync guard:按活动扫描配置提示本会话内尚未提交的项目改动。 emit_empty() { printf '{}\n' } cat >/dev/null if [ -n "${CURSOR_SKIP_CHANGELOG:-}" ]; then emit_empty exit 0 fi script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" 2>/dev/null && pwd) cursor_dir=$(dirname -- "$script_dir") repo_root=$(dirname -- "$cursor_dir") changelog="$cursor_dir/changelog/changelog-headlines.md" ack_file="$cursor_dir/changelog/.changelog-ack" session_marker="$cursor_dir/changelog/.session-start" scan_config="$script_dir/changelog-scan.json" if [ ! -f "$changelog" ] || [ ! -f "$scan_config" ]; then emit_empty exit 0 fi if [ ! -f "$session_marker" ]; then emit_empty exit 0 fi get_mtime() { stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null } if [ -f "$ack_file" ]; then changelog_mtime=$(get_mtime "$changelog") ack_mtime=$(get_mtime "$ack_file") if [ -n "$changelog_mtime" ] && [ -n "$ack_mtime" ] && [ "$ack_mtime" -ge "$changelog_mtime" ] 2>/dev/null; then emit_empty exit 0 fi fi python_cmd='' for candidate in python3 python; do if command -v "$candidate" >/dev/null 2>&1 && "$candidate" -c 'import json' >/dev/null 2>&1; then python_cmd=$candidate break fi done if [ -z "$python_cmd" ]; then emit_empty exit 0 fi result=$( "$python_cmd" - "$repo_root" "$changelog" "$session_marker" "$scan_config" <<'PY' import json import os from pathlib import Path import subprocess import sys repo_root = Path(sys.argv[1]).resolve() changelog = Path(sys.argv[2]) session_marker = Path(sys.argv[3]) scan_config = Path(sys.argv[4]) def emit(value): print(json.dumps(value, ensure_ascii=False, separators=(",", ":"))) try: config = json.loads(scan_config.read_text(encoding="utf-8")) if config.get("schema_version") != 1: raise ValueError("unsupported schema") names = { value.casefold() for value in config.get("included_names", []) if isinstance(value, str) and value } extensions = { value.casefold() for value in config.get("included_extensions", []) if isinstance(value, str) and value } excluded = { value.casefold() for value in config.get("excluded_directories", []) if isinstance(value, str) and value } if not names and not extensions: emit({}) raise SystemExit(0) status = subprocess.run( ["git", "-C", str(repo_root), "status", "--porcelain=v1", "--untracked-files=all", "--"], check=False, capture_output=True, text=True, encoding="utf-8", ) if status.returncode != 0 or not status.stdout.strip(): emit({}) raise SystemExit(0) comparison_mtime = max( changelog.stat().st_mtime, session_marker.stat().st_mtime, ) for current_root, directory_names, file_names in os.walk(repo_root): directory_names[:] = [ name for name in directory_names if name.casefold() not in excluded ] current = Path(current_root) for file_name in file_names: candidate = current / file_name if ( file_name.casefold() not in names and candidate.suffix.casefold() not in extensions ): continue try: if candidate.stat().st_mtime <= comparison_mtime: continue except OSError: continue relative = candidate.relative_to(repo_root).as_posix() path_status = subprocess.run( ["git", "-C", str(repo_root), "status", "--porcelain=v1", "--untracked-files=all", "--", relative], check=False, capture_output=True, text=True, encoding="utf-8", ) if path_status.returncode == 0 and path_status.stdout.strip(): message = ( "[Hook] Project source/config has an uncommitted change newer " f"than this session and the changelog (e.g. {relative}). " "Run dev-changelog Skill operation A now: add a fragment under " ".cursor/changelog/entries/ and rebuild the changelog views." ) emit({"followup_message": message}) raise SystemExit(0) emit({}) except SystemExit: raise except Exception: emit({}) PY ) || result='{}' if [ -n "$result" ]; then printf '%s\n' "$result" else emit_empty fi exit 0