Files
prompts/script/install
T
clawbot f4275b9ac0
check / check (push) Successful in 26s
script/install: stop on a repo's own script of the same name
script/install replaced any tracked script/install or
script/update-policy in the target, including a repo's own unrelated
script. It now stops when such a file has a line the canonical file
has never held. An older shared copy is still replaced.

The padded file in the self-replacement test is now an earlier
canonical version, so the new stop does not catch it.

Model: opus-5-5
2026-11-09 18:49:16 +00:00

263 lines
8.8 KiB
Bash
Executable File

#!/bin/sh
# script/install: write the current policy files from the prompts repo
# into a repo clone, over HTTP. It commits nothing; script/update-policy
# does that. Identical in all repos. Our own extension to
# scripts-to-rule-them-all.
#
# Usage: script/install [path of a repo clone]
# Without a path it updates the repo this script sits in.
#
# Standard output is the full hash of the prompts commit, then each path
# written, one per line. Messages go to standard error.
#
# The script writes nothing until every file has been fetched and
# merged. A repo's own entries are kept:
# - .gitignore, .editorconfig, .dockerignore: the lines below the
# canonical file's last line. An entry above that line which the
# canonical file has never held stops the script.
# - .golangci.yml: the entries the repo added to the deny list of the
# test-support rule. Any other line the canonical file has never
# held stops the script, unless it is blank or a comment.
set -eu
PROMPTS_URL="https://git.eeqj.de/sneak/prompts"
PROMPTS_API_URL="https://git.eeqj.de/api/v1/repos/sneak/prompts"
PROMPTS_FIRST_COMMIT="af257bef63f162a681bddc80d9b941c47a44b748"
# Paths in the prompts repo. A file the target repo does not carry is
# skipped, except the two scripts, which every repo gets.
VENDORED_FILES="
.dockerignore
.editorconfig
.gitea/workflows/check.yml
.gitignore
.golangci.yml
.prettierrc
prompts/API_POLICIES.md
prompts/CODE_STYLEGUIDE.md
prompts/CODE_STYLEGUIDE_GO.md
prompts/CODE_STYLEGUIDE_JS.md
prompts/CODE_STYLEGUIDE_PYTHON.md
prompts/EXISTING_REPO_CHECKLIST.md
prompts/GO_HTTP_SERVER_CONVENTIONS.md
prompts/LLM_PROSE_TELLS.md
prompts/NEW_REPO_CHECKLIST.md
prompts/REPO_POLICIES.md
script/install
script/update-policy
"
die() {
echo "install: $*" >&2
exit 1
}
# fetch <url> <output file>
fetch() {
curl --fail --silent --show-error --max-time 60 --output "$2" "$1" ||
die "cannot fetch $1"
}
# local_path <path in prompts>: print where the target repo keeps the
# file, or nothing if git does not track it there. A prompts/NAME.md
# lands at docs/NAME.md or NAME.md.
local_path() {
name="${1#prompts/}"
if [ "$name" != "$1" ] && [ -n "$(git ls-files -- "docs/$name")" ]; then
echo "docs/$name"
elif [ -n "$(git ls-files -- "$name")" ]; then
echo "$name"
fi
}
# former_lines <patch> <path in prompts>: print the lines earlier commits
# removed from the file. The file's section of the patch ends at the
# next diff or the next commit.
former_lines() {
awk -v header="diff --git a/$2 b/$2" '
/^diff --git / { inside = ($0 == header); next }
/^From / { inside = 0; next }
inside && /^-/ { print substr($0, 2) }
' "$1"
}
# local_only_lines <fetched> <local> [end]: print each line of the local
# file, above line end if given, that the canonical file has never held:
# not in the fetched file or in <fetched>.former, the lines earlier
# commits removed from it. Blank lines and comments do not count.
local_only_lines() {
cat "$1" "$1.former" | awk -v end="${3:-}" '
NR == FNR { held[$0]; next }
(end == "" || FNR < end) && !/^[ \t]*(#|$)/ && !($0 in held)
' - "$2"
}
# keep_lines_below_last <fetched> <local>: append to the fetched file
# whatever follows its last line in the local file. Stop if the canonical
# file has never held a line above that one, unless it is blank or a
# comment: replacing the file would delete it.
keep_lines_below_last() {
last="$(tail -n 1 "$1")"
number="$(grep -n -F -x -- "$last" "$2" | head -n 1 | cut -d: -f1)"
if [ -z "$last" ] || [ -z "$number" ]; then
die "$2 lacks this line; add it and put the repo's own entries below it: $last"
fi
own="$(local_only_lines "$1" "$2" "$number")"
if [ -n "$own" ]; then
die "$2 has entries above its last canonical line that the canonical file lacks; move each below that line to keep it, or delete it:
$own"
fi
tail -n "+$((number + 1))" "$2" >> "$1"
}
# keep_deny_entries <fetched> <local>: add to the deny list of the
# test-support rule in the fetched file the entries only the local file
# has. An entry is a "- pkg:" line, the lines under it and any comment
# above it. Stop if the local file still has a line that neither the
# result nor the canonical file has ever held, unless it is blank or a
# comment: replacing the file would delete it.
keep_deny_entries() {
awk '
function indent(line) { return match(line, /[^ ]/) - 1 }
function print_local_entries(i) {
for (i = 1; i <= count; i++)
if (!(pkg[i] in fetched_pkg))
printf "%s", entry[i]
}
FNR == 1 { in_rule = 0; in_list = 0 }
in_list && /[^ ]/ && indent($0) <= list_indent {
in_list = 0
if (FILENAME == ARGV[2]) print_local_entries()
}
in_list {
is_entry = ($1 == "-" && $2 == "pkg:")
if (FILENAME == ARGV[2]) {
if (is_entry) fetched_pkg[$3]
print
} else if (is_entry) {
count++
pkg[count] = $3
entry[count] = comment $0 "\n"
comment = ""
} else if (/^ *#/) {
comment = comment $0 "\n"
} else if (count) {
entry[count] = entry[count] $0 "\n"
}
next
}
in_rule && /^ *[^ #]/ && indent($0) <= rule_indent { in_rule = 0 }
/^ *test-support:/ { in_rule = 1; rule_indent = indent($0) }
in_rule && /^ *deny:/ {
in_rule = 0
in_list = 1
list_indent = indent($0)
if (FILENAME == ARGV[2]) found = 1
}
FILENAME == ARGV[2] { print }
END {
if (in_list) print_local_entries()
exit !found
}
' "$2" "$1" > "$1.merged" ||
die "the fetched $2 has no deny list in its test-support rule"
mv "$1.merged" "$1"
own="$(local_only_lines "$1" "$2")"
if [ -n "$own" ]; then
die "$2 has lines the canonical file has never held; only the deny list of the test-support rule may differ from canonical:
$own"
fi
}
# stop_on_own_script <fetched> <local>: stop if the local script has a
# line the canonical file has never held, unless it is blank or a
# comment: the repo wrote that script itself, and replacing it would
# delete it. A repo without the script is fine.
stop_on_own_script() {
[ -e "$2" ] || return 0
own="$(local_only_lines "$1" "$2")"
if [ -n "$own" ]; then
die "$2 has lines the canonical file has never held; rename the repo's own script to keep it:
$own"
fi
}
main() {
[ "$#" -le 1 ] || die "usage: script/install [path of a repo clone]"
target="${1:-$(dirname "$0")/..}"
cd "$target" || die "no such directory: $target"
if [ "$(git rev-parse --show-toplevel 2> /dev/null)" != "$(pwd -P)" ]; then
die "not the top of a git clone: $target"
fi
if [ -e prompts/REPO_POLICIES.md ]; then
die "this repo is the source of the policy files"
fi
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
fetch "$PROMPTS_API_URL/branches/main" "$tmp/branch.json"
commit="$(grep -Eo '"id":"[0-9a-f]{40}"' "$tmp/branch.json" |
head -n 1 | cut -d'"' -f4)"
[ -n "$commit" ] || die "no commit hash in $PROMPTS_API_URL/branches/main"
fetch "$PROMPTS_URL/compare/$PROMPTS_FIRST_COMMIT...$commit.patch" \
"$tmp/history.patch"
carried=""
for file in $VENDORED_FILES; do
case "$file" in
script/*) path="$file" ;;
*) path="$(local_path "$file")" ;;
esac
[ -n "$path" ] || continue
if [ -n "$(git status --porcelain -- "$path")" ]; then
die "$path has uncommitted changes"
fi
mkdir -p "$tmp/$(dirname "$path")"
fetch "$PROMPTS_URL/raw/commit/$commit/$file" "$tmp/$path"
former_lines "$tmp/history.patch" "$file" > "$tmp/$path.former"
carried="$carried $path"
done
for path in $carried; do
case "$path" in
.gitignore | .editorconfig | .dockerignore)
keep_lines_below_last "$tmp/$path" "$path"
;;
.golangci.yml)
keep_deny_entries "$tmp/$path" "$path"
;;
script/*)
stop_on_own_script "$tmp/$path" "$path"
;;
esac
done
echo "$commit"
for path in $carried; do
case "$path" in
script/*)
# A new name and mv, because script/update-policy may
# be the script running.
mkdir -p script
cp "$tmp/$path" "$path.new"
chmod 755 "$path.new"
mv "$path.new" "$path"
;;
*)
cp "$tmp/$path" "$path"
;;
esac
echo "$path"
done
}
main "$@"