112 lines
4.2 KiB
Bash
Executable File
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"
|