Files
Kill_LIFE/tools/specs/sync_spec_mirror.sh

193 lines
4.1 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
CANONICAL_DIR="${CANONICAL_SPECS_DIR:-${ROOT_DIR}/specs}"
MIRROR_DIR="${MIRROR_SPECS_DIR:-${ROOT_DIR}/ai-agentic-embedded-base/specs}"
REPORT_FILE="${REPORT_FILE:-${ROOT_DIR}/artifacts/specs/mirror_sync_report.md}"
COMMAND=""
VERBOSE=0
YES=0
usage() {
cat <<'EOF'
Usage: bash tools/specs/sync_spec_mirror.sh <check|sync|all> [options]
Synchronize the exported `spec_kit` mirror with the canonical `specs/` tree.
Commands:
check Compare canonical specs and mirror, then write a Markdown report
sync Synchronize the mirror from canonical specs
all Check, sync if needed, then re-check
Options:
--report PATH Override the Markdown report path
--yes Allow mirror updates
--verbose Print progress logs
-h, --help Show this help
Env overrides:
CANONICAL_SPECS_DIR
MIRROR_SPECS_DIR
REPORT_FILE
Examples:
bash tools/specs/sync_spec_mirror.sh check
bash tools/specs/sync_spec_mirror.sh sync --yes
bash tools/specs/sync_spec_mirror.sh all --yes
EOF
}
log() {
if [[ "${VERBOSE}" == "1" ]]; then
printf '[sync-spec-mirror] %s\n' "$*"
fi
}
die() {
printf '%s\n' "$*" >&2
exit 1
}
require_rsync() {
command -v rsync >/dev/null 2>&1 || die "Missing dependency: rsync"
}
drift_output() {
rsync -ani --delete "${CANONICAL_DIR%/}/" "${MIRROR_DIR%/}/" | sed '/^$/d'
}
write_report() {
local status="$1"
local action="$2"
local drift="$3"
local report_dir
report_dir="$(dirname -- "${REPORT_FILE}")"
mkdir -p "${report_dir}"
{
printf '# Spec mirror sync report\n\n'
printf 'Generated: %s\n\n' "$(date '+%Y-%m-%d %H:%M:%S %z')"
printf '## Summary\n\n'
printf -- '- Status: `%s`\n' "${status}"
printf -- '- Action: `%s`\n' "${action}"
printf -- '- Canonical: `%s`\n' "${CANONICAL_DIR#${ROOT_DIR}/}"
printf -- '- Mirror: `%s`\n' "${MIRROR_DIR#${ROOT_DIR}/}"
printf -- '- Report path: `%s`\n\n' "${REPORT_FILE#${ROOT_DIR}/}"
printf '## Drift\n\n'
if [[ -n "${drift}" ]]; then
printf '```text\n%s\n```\n' "${drift}"
else
printf -- '- None.\n'
fi
} > "${REPORT_FILE}"
}
run_repo_validation() {
if [[ "${CANONICAL_DIR}" == "${ROOT_DIR}/specs" && "${MIRROR_DIR}" == "${ROOT_DIR}/ai-agentic-embedded-base/specs" ]]; then
python3 "${ROOT_DIR}/tools/validate_specs.py" --require-mirror-sync >/dev/null
fi
}
run_check() {
local drift
require_rsync
[[ -d "${CANONICAL_DIR}" ]] || die "Missing canonical specs directory: ${CANONICAL_DIR}"
mkdir -p "${MIRROR_DIR}"
drift="$(drift_output)"
if [[ -n "${drift}" ]]; then
write_report "drift" "check" "${drift}"
log "Mirror drift detected"
return 1
fi
write_report "clean" "check" ""
log "Mirror is synchronized"
return 0
}
run_sync() {
local drift_after
require_rsync
[[ -d "${CANONICAL_DIR}" ]] || die "Missing canonical specs directory: ${CANONICAL_DIR}"
mkdir -p "${MIRROR_DIR}"
[[ "${YES}" == "1" ]] || die "Refusing to update the mirror without --yes."
log "Synchronizing mirror"
rsync -a --delete "${CANONICAL_DIR%/}/" "${MIRROR_DIR%/}/"
drift_after="$(drift_output)"
if [[ -n "${drift_after}" ]]; then
write_report "drift" "sync-failed" "${drift_after}"
die "Mirror sync did not converge. See ${REPORT_FILE}."
fi
run_repo_validation
write_report "clean" "sync" ""
log "Mirror synchronized"
}
if [[ $# -eq 0 ]]; then
usage >&2
exit 2
fi
COMMAND="$1"
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--report)
shift
[[ $# -gt 0 ]] || die "Missing value for --report"
REPORT_FILE="$1"
;;
--yes)
YES=1
;;
--verbose)
VERBOSE=1
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
shift
done
case "${COMMAND}" in
check)
if run_check; then
exit 0
fi
exit 1
;;
sync)
run_sync
;;
all)
if run_check; then
exit 0
fi
run_sync
run_check
;;
*)
usage >&2
exit 2
;;
esac