Files
2026-07-13 13:05:52 +08:00

112 lines
4.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="${1:-"$ROOT/dist/kami.zip"}"
case "$OUT" in
/*) ;;
*) OUT="$ROOT/$OUT" ;;
esac
PACKAGE_ROOT_NAME="${KAMI_PACKAGE_ROOT_NAME:-kami}"
PACKAGE_MAX_BYTES="${KAMI_PACKAGE_MAX_BYTES:-6000000}"
PACKAGE_FORBIDDEN_RE='^(\.agents/|\.claude/|\.claude-plugin/|\.github/|plugins/|assets/(showcase|demos|examples|illustrations)/|assets/images/[123]\.png$|assets/fonts/TsangerJinKai02-W0[45]\.ttf$|assets/fonts/SourceHanSerifKR-(Regular|Medium)\.otf$|dist/|index(-[^/]+)?\.html$|styles\.css$|llms\.txt$|robots\.txt$|sitemap\.xml$|vercel\.json$|AGENTS\.md$|CLAUDE\.md$|README\.md$|\.gitignore$|scripts/(build_metadata|draft-release-notes|package-skill)\.py$|scripts/package-skill\.sh$|scripts/tests/)'
PACKAGE_REQUIRED_ENTRIES=(
"SKILL.md"
"CHEATSHEET.md"
"VERSION"
"LICENSE"
"assets/images/logo.svg"
"assets/fonts/JetBrainsMono.woff2"
"assets/templates/resume.html"
"assets/templates/landing-page.html"
"assets/diagrams/sequence.html"
"references/design.md"
"scripts/build.py"
"scripts/ensure-fonts.sh"
"scripts/site_facts.py"
)
mkdir -p "$(dirname "$OUT")"
rm -f "$OUT"
cd "$ROOT"
MANIFEST="$(mktemp)"
FILTERED_MANIFEST="$(mktemp)"
ZIP_MANIFEST="$(mktemp)"
STAGING="$(mktemp -d)"
trap 'rm -f "$MANIFEST" "$FILTERED_MANIFEST" "$ZIP_MANIFEST"; rm -rf "$STAGING"' EXIT
git ls-files > "$MANIFEST"
awk '
/(^|\/)__pycache__\// { next }
/\.pyc$/ { next }
/(^|\/)\.DS_Store$/ { next }
/^(SKILL\.md|CHEATSHEET\.md|VERSION|LICENSE)$/ { print; next }
/^assets\/templates\// { print; next }
/^assets\/diagrams\// { print; next }
/^assets\/images\/logo\.svg$/ { print; next }
/^assets\/fonts\/JetBrainsMono\.woff2$/ { print; next }
/^assets\/fonts\/LICENSE-SourceHanSerifK\.txt$/ { print; next }
/^references\// { print; next }
/^scripts\/(build|check-update|checks|ensure-fonts|highlight|lint|mermaid_normalize|optional_deps|shared|site_facts|tokens|verify)\.(py|sh)$/ { print; next }
' "$MANIFEST" > "$FILTERED_MANIFEST"
# Coverage gate: every tracked scripts/ file must be either packaged by the
# allowlist above or named in the repo-only exclusion below. Without this, a
# new runtime module that build.py imports would silently miss the zip and
# the installed skill would ImportError while every local check stays green.
SCRIPTS_REPO_ONLY_RE='^scripts/(build_metadata\.py|draft-release-notes\.py|package-skill\.sh|tests/)'
unaccounted="$(grep '^scripts/' "$MANIFEST" \
| grep -Ev "$SCRIPTS_REPO_ONLY_RE" \
| grep -Fvx -f <(grep '^scripts/' "$FILTERED_MANIFEST" || true) || true)"
if [ -n "$unaccounted" ]; then
echo "ERROR: tracked scripts neither packaged nor listed as repo-only:" >&2
printf '%s\n' "$unaccounted" >&2
echo "Add them to the packaging allowlist or to SCRIPTS_REPO_ONLY_RE." >&2
exit 1
fi
while IFS= read -r entry; do
dest="$STAGING/$PACKAGE_ROOT_NAME/$entry"
mkdir -p "$(dirname "$dest")"
cp -p "$entry" "$dest"
done < "$FILTERED_MANIFEST"
(
cd "$STAGING"
find "$PACKAGE_ROOT_NAME" -type f | sort > "$ZIP_MANIFEST"
zip -X -q "$OUT" -@ < "$ZIP_MANIFEST"
)
entries="$(zipinfo -1 "$OUT")"
bad_root="$(printf '%s\n' "$entries" | awk -v prefix="${PACKAGE_ROOT_NAME}/" 'index($0, prefix) != 1 { print }')"
if [ -n "$bad_root" ]; then
echo "ERROR: package entries must live under ${PACKAGE_ROOT_NAME}/:" >&2
printf '%s\n' "$bad_root" >&2
exit 1
fi
stripped_entries="$(printf '%s\n' "$entries" | sed "s#^${PACKAGE_ROOT_NAME}/##")"
if forbidden_entries="$(printf '%s\n' "$stripped_entries" | grep -E "$PACKAGE_FORBIDDEN_RE")"; then
echo "ERROR: disallowed package entry found in $OUT:" >&2
printf '%s\n' "$forbidden_entries" >&2
exit 1
fi
for required in "${PACKAGE_REQUIRED_ENTRIES[@]}"; do
if ! printf '%s\n' "$entries" | grep -Fxq "${PACKAGE_ROOT_NAME}/${required}"; then
echo "ERROR: required package entry missing from $OUT: ${PACKAGE_ROOT_NAME}/${required}" >&2
exit 1
fi
done
size_bytes="$(wc -c < "$OUT" | tr -d '[:space:]')"
if (( size_bytes > PACKAGE_MAX_BYTES )); then
echo "ERROR: package exceeds ${PACKAGE_MAX_BYTES} bytes: ${size_bytes} bytes" >&2
exit 1
fi
echo "OK: package audit passed (${size_bytes} bytes, limit ${PACKAGE_MAX_BYTES})"
echo "OK: wrote $OUT"