diff --git a/.gitea/workflows/bundle.yml b/.gitea/workflows/bundle.yml
new file mode 100644
index 0000000..0e816bc
--- /dev/null
+++ b/.gitea/workflows/bundle.yml
@@ -0,0 +1,37 @@
+name: bundle
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "22 9 * * *" # daily 09:22 UTC
+
+jobs:
+ bundle:
+ runs-on: [debian-13]
+ steps:
+ - name: Checkout knoxmakers-inkscape
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Configure git identity
+ run: |
+ git config user.name "haxbot"
+ git config user.email "haxbot@knoxmakers.sh"
+
+ - name: Run bundle script
+ run: |
+ bash scripts/bundle.sh
+
+ - name: Push (if a bundle update commit was created)
+ env:
+ HAXBOT_TOKEN: ${{ secrets.HAXBOT_TOKEN }}
+ run: |
+ if git log -1 --pretty=%B | grep -q '^bundle: update'; then
+ REPO_URL="https://x-access-token:${HAXBOT_TOKEN}@git.knoxmakers.org/KnoxMakers/knoxmakers-inkscape.git"
+ git remote set-url origin "$REPO_URL"
+ git push origin HEAD:main
+ else
+ echo "No update commit created."
+ fi
+
diff --git a/scripts/bundle.sh b/scripts/bundle.sh
new file mode 100755
index 0000000..e3c7ad3
--- /dev/null
+++ b/scripts/bundle.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+EXT_DIR="$ROOT/extensions"
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+
+mkdir -p "$EXT_DIR"
+
+# Format: name|repo_url|branch|dest_subdir_under_extensions
+REPOS=(
+ "boxbot3000|https://github.com/jondale/boxbot3000.git|main|boxbot3000"
+ "km-living-hinge|https://github.com/KnoxMakers/km-living-hinge.git|main|km-living-hinge"
+ "km-plot|https://git.knoxmakers.org/KnoxMakers/km-plot.git|main|km-plot"
+)
+
+sync_one() {
+ local name="$1" url="$2" branch="$3" dest="$4"
+ local work="$TMP/$name"
+ local target="$EXT_DIR/$dest"
+
+ echo "==> Sync $name from $url ($branch) -> extensions/$dest"
+
+ git clone --depth 1 --branch "$branch" "$url" "$work"
+
+ rm -rf "$target"
+ mkdir -p "$target"
+ cp -a "$work/." "$target/"
+ rm -rf "$target/.git"
+
+ # Provenance
+ (cd "$work" && git rev-parse HEAD) > "$target/.upstream_commit"
+ echo "$url" > "$target/.upstream_url"
+ echo "$branch" > "$target/.upstream_branch"
+}
+
+post_process() {
+ echo "==> Post-processing bundled extensions"
+
+ local inx="$EXT_DIR/boxbot3000/botbox.inx"
+ if [[ ! -f "$inx" ]]; then
+ echo "ERROR: expected file not found: $inx"
+ exit 1
+ fi
+
+ echo "Patching $inx"
+
+ # 1) Set the ...
+ sed -i 's|[^<]*|org.knoxmakers.botbox|' "$inx"
+
+ # 2) Replace the ... block
+ sed -i '//,/<\/effectsmenu>/c\
+\
+ \
+ \
+ \
+' "$inx"
+}
+
+main() {
+ # Sync phase
+ for entry in "${REPOS[@]}"; do
+ IFS='|' read -r name url branch dest <<<"$entry"
+ sync_one "$name" "$url" "$branch" "$dest"
+ done
+
+ # Post-processing phase
+ post_process
+
+ # Commit phase
+ cd "$ROOT"
+ git add extensions
+
+ if git diff --cached --quiet; then
+ echo "No bundle changes."
+ exit 0
+ fi
+
+ git commit -m "bundle: update ($(date -u +%Y-%m-%d))"
+}
+
+main "$@"
+