#!/usr/bin/env bash # Step 1 — Preflight, archive tag, and the plugin map. # Asserts tooling + repo hygiene, builds the dedicated extraction mirror ($WORK/_mirror) # via `git clone --no-local` (R4/M6 — extraction never reads the working checkout), # enumerates >1MB blob-strip candidates in ms-ai-architect and emits blob_strip_safe # into plugin-map.json (M4/F3), and creates the local archive tag (D2). # NULL push (D8): never pushes, never mutates plugins/ shared/ marketplace.json. # # Usage: 00-preflight.sh [--dry-run] # --dry-run : validate tooling + map + print the 11-target table; NO mirror, NO tag, NO map mutation. # # Bash 3.2 compatible (no associative arrays / mapfile). Override WORK= to relocate the workspace. set -euo pipefail DRY_RUN=0 for arg in "$@"; do case "$arg" in --dry-run) DRY_RUN=1 ;; *) echo "preflight: unknown arg: $arg" >&2; exit 2 ;; esac done SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MAP="$SCRIPT_DIR/plugin-map.json" REPO_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel)" WORK="${WORK:-/tmp/polyrepo-migration}" MIRROR="$WORK/_mirror" ARCHIVE_TAG="pre-polyrepo-archive" BASELINE="f2d41c8" fail() { printf 'PREFLIGHT FAIL: %s\n' "$*" >&2; exit 1; } # --- tooling --- git filter-repo --version >/dev/null 2>&1 || fail "git filter-repo not available — brew install git-filter-repo" command -v python3 >/dev/null 2>&1 || fail "python3 not found" python3 -c 'import sys; sys.exit(0 if sys.version_info[:2] >= (3, 6) else 1)' \ || fail "python3 >= 3.6 required" # --- map presence + parse + count --- [ -f "$MAP" ] || fail "plugin-map.json missing at $MAP" python3 -c "import json; json.load(open('$MAP'))" >/dev/null 2>&1 || fail "plugin-map.json does not parse" TARGET_COUNT="$(python3 -c "import json; print(len(json.load(open('$MAP'))['targets']))")" [ "$TARGET_COUNT" = "11" ] || fail "expected 11 targets in plugin-map.json, found $TARGET_COUNT" # --- path hygiene (aeb6292): every target path must be whitespace- and glob-free, so the space-joined # `for p in $(mappaths …)` word-splitting in 99-dryrun.sh (live_files / SC6 baseline) is sound. --- python3 - "$MAP" <<'PY' || fail "plugin-map.json has a path with whitespace or a glob metacharacter (breaks word-split path handling in 99-dryrun.sh)" import json, re, sys m = json.load(open(sys.argv[1])) bad = [] for k, t in m["targets"].items(): for p in t.get("paths", []): if re.search(r"\s", p) or any(c in p for c in "*?[]"): bad.append("%s: %r" % (k, p)) if bad: sys.stderr.write("offending paths:\n " + "\n ".join(bad) + "\n") sys.exit(1) PY # --- repo baseline (HEAD descends from the ratified brief commit, on main, not behind remote) --- BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)" [ "$BRANCH" = "main" ] || fail "not on main (HEAD on '$BRANCH')" git -C "$REPO_ROOT" merge-base --is-ancestor "$BASELINE" HEAD \ || fail "HEAD does not descend from baseline $BASELINE" if git -C "$REPO_ROOT" rev-parse --verify -q origin/main >/dev/null; then BEHIND="$(git -C "$REPO_ROOT" rev-list --count HEAD..origin/main)" [ "$BEHIND" = "0" ] || fail "behind origin/main by $BEHIND commit(s) — pull/rebase before migrating" AHEAD="$(git -C "$REPO_ROOT" rev-list --count origin/main..HEAD)" echo " baseline: descends $BASELINE on main; $AHEAD local commit(s) ahead of origin/main (unpushed — expected under D8)" else echo " baseline: descends $BASELINE on main (no origin/main ref present)" fi # --- hygiene (M12): tolerate untracked stray docs; abort only on a TRACKED uncommitted # change to a migration-sensitive surface. The literal `git status --porcelain plugins ...` # would also list untracked (??) files, which M12 explicitly tolerates, so we filter them. --- DIRTY="$(git -C "$REPO_ROOT" status --porcelain plugins shared .claude-plugin/marketplace.json scripts \ | grep -v '^??' || true)" [ -z "$DIRTY" ] || fail "uncommitted change to a migration-sensitive surface: $DIRTY" # --- print the 11-target table --- echo "PREFLIGHT OK" python3 - "$MAP" <<'PY' import json, sys m = json.load(open(sys.argv[1])) t = m["targets"] print(" %-26s %-9s %-5s %-7s %-5s %s" % ("target", "tag", "paths", "vendor", "blob", "repo_url")) for k in sorted(t): e = t[k] print(" %-26s %-9s %-5d %-7s %-5s %s" % ( k, e["tag"], len(e["paths"]), str(e["has_vendor_ds"]), str(e.get("blob_strip", False)), e["repo_url"])) print(" drop:", ", ".join(m.get("drop", [])) or "(none)") print(" targets:", len(t)) PY if [ "$DRY_RUN" = "1" ]; then echo " (--dry-run: no mirror clone, no archive tag, no map mutation)" exit 0 fi # --- full run: build the dedicated extraction mirror once (R4/M6) --- mkdir -p "$WORK" if [ -e "$MIRROR/HEAD" ] || [ -d "$MIRROR/.git" ]; then echo " mirror: present at $MIRROR (reused)" else rm -rf "$MIRROR" git clone --no-local --quiet "$REPO_ROOT" "$MIRROR" echo " mirror: built $MIRROR via clone --no-local (pinned at $(git -C "$MIRROR" rev-parse --short HEAD))" fi # --- enumerate >1MB blobs in ms-ai-architect history; safe iff all are screenshots (M4/F3) --- BLOBLIST="$(git -C "$MIRROR" rev-list --objects --all \ | git -C "$MIRROR" cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \ | awk '$1=="blob" && $3+0>1048576 { p=""; for (i=4;i<=NF;i++) p = p (i>4?" ":"") $i; print $3"\t"p }' \ | grep -E 'plugins/ms-ai-architect/' || true)" SAFE=true if [ -n "$BLOBLIST" ]; then NONSCREEN="$(printf '%s\n' "$BLOBLIST" | grep -vE 'plugins/ms-ai-architect/playground/screenshots/' || true)" [ -z "$NONSCREEN" ] || SAFE=false fi echo " blob-strip candidates (>1MB) in ms-ai-architect history:" if [ -n "$BLOBLIST" ]; then printf '%s\n' "$BLOBLIST" | sed 's/^/ /'; else echo " (none)"; fi echo " blob_strip_safe = $SAFE" # --- idempotent emit into plugin-map.json (only rewrites if the value actually changed) --- python3 - "$MAP" "$SAFE" <<'PY' import json, sys path, safe = sys.argv[1], (sys.argv[2] == "true") m = json.load(open(path)) cur = m["targets"].get("ms-ai-architect", {}).get("blob_strip_safe") if cur != safe: m["targets"]["ms-ai-architect"]["blob_strip_safe"] = safe with open(path, "w") as f: f.write(json.dumps(m, indent=2) + "\n") print(" plugin-map.json: blob_strip_safe set to", safe) else: print(" plugin-map.json: blob_strip_safe already", safe, "(no change)") PY # --- create the local annotated archive tag idempotently (D2; pushed later, in the window) --- if git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$ARCHIVE_TAG" >/dev/null; then echo " tag: $ARCHIVE_TAG already exists (idempotent)" else git -C "$REPO_ROOT" tag -a "$ARCHIVE_TAG" -m "Archive of the monorepo before the polyrepo split (D2)" echo " tag: created $ARCHIVE_TAG at HEAD (local only — pushed in the operator window)" fi echo "PREFLIGHT COMPLETE"