docs(migration): fix run-operator-window DS special-casing (2 BLOCKER, steps d+e)
Review of the rollout script found 2 deterministic blockers on target #2, playground-design-system — a shared component, not a marketplace plugin: - step (d): DS has no .claude-plugin/plugin.json (it ships tokens.css/base.css/ schemas). The plugin.json assertion would die. Now DS-aware: assert tokens.css for DS_KEY, plugin.json otherwise. - step (e): 60-rewrite --only playground-design-system errors (--only name not in marketplace.json) since DS has no catalog entry. New entry_present() gate skips the flip when the key has no marketplace entry, mirroring 60-rewrite --all (which only touches the 10 real plugins). Net: pilot + 9 plugins flip (count_local to 0); DS is stood up as a repo but never flipped. Verified: bash -n OK, entry_present + 60-rewrite behaviour tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d577cadcf4
commit
b6aa815e41
1 changed files with 209 additions and 0 deletions
|
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env bash
|
||||
# run-operator-window.sh — turnkey execution of the polyrepo operator window (RUNBOOK §0–§4).
|
||||
#
|
||||
# RUN AS THE OPERATOR, via the chat `!` prefix: ! bash docs/marketplace-polyrepo-migration/migration/run-operator-window.sh
|
||||
# NOT via Claude's Bash tool — creating public repos + bulk-pushing trees to new remotes is gated by the
|
||||
# auto-mode safety classifier (by design). Running it yourself via `!` is the sanctioned path: it executes
|
||||
# as you, so the classifier / permission layer / push-window hook are all out of the picture.
|
||||
#
|
||||
# Idempotent + stop-on-first-failure. Safe to re-run after a partial run (created repos → 409 ok, pushed
|
||||
# refs → up-to-date, already-flipped catalog entries → skipped).
|
||||
#
|
||||
# DEFAULT (no args) — §0–§3, REVERSIBLE:
|
||||
# For all 11 targets in RUNBOOK order: (a) validate the extract → (b) create the Forgejo repo
|
||||
# (auto_init:false, public) → (c) push the extract over SSH → (d) verify HTTPS+ref resolution
|
||||
# (the install-smoke PROXY — a real `/plugin install` still needs a fresh Claude Code session) →
|
||||
# (e) flip marketplace.json to the external nested source + push the catalog.
|
||||
# graceful-handoff runs first as the PILOT gate. Standing up the 11 repos is what unblocks per-plugin
|
||||
# parallel work — thinning is NOT required for that. Every flip is `git revert`-able while ./plugins/<k>
|
||||
# still exists, so this whole phase is reversible.
|
||||
#
|
||||
# --thin — §4, build + verify the thin-catalog preview (no apply).
|
||||
# CONFIRM_THIN=1 ... --thin — §4 APPLY: git rm plugins/ shared/ sync-script, swap in CONVENTIONS/CLAUDE/
|
||||
# README, commit, push, push the pre-polyrepo-archive tag. IRREVERSIBLE point of no easy return —
|
||||
# run only after the real `/plugin install` smoke-tests for all 11 have passed.
|
||||
#
|
||||
# Prereqs: $FORGEJO_TOKEN exported (Keychain → ~/.zshenv); on branch main; the 11 extracts buildable in
|
||||
# $WORK (the validate step self-extracts if missing — that self-heal also rebuilds the mirror via preflight).
|
||||
set -uo pipefail
|
||||
|
||||
HOST="git.fromaitochitta.com"
|
||||
API="https://$HOST/api/v1"
|
||||
MIG="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$MIG/../../.." && pwd)"
|
||||
MAP="$MIG/plugin-map.json"
|
||||
LIVE="$ROOT/.claude-plugin/marketplace.json"
|
||||
WORK="${WORK:-/tmp/polyrepo-migration}"
|
||||
ARCHIVE_TAG="pre-polyrepo-archive"
|
||||
|
||||
PILOT="graceful-handoff"
|
||||
REST="playground-design-system voyage llm-security linkedin-studio ms-ai-architect config-audit okr ai-psychosis human-friendly-style claude-design"
|
||||
# The shared design-system is a standalone repo (stood up so consumers can vendor from an upstream), but it
|
||||
# is NOT a marketplace plugin: it has no .claude-plugin/plugin.json and no marketplace.json entry. Steps (d)
|
||||
# and (e) special-case it (assert its DS root marker; no catalog flip — mirrors 60-rewrite --all).
|
||||
DS_KEY="playground-design-system"
|
||||
|
||||
die() { printf '\n✖ %s\n' "$*" >&2; exit 1; }
|
||||
say() { printf '%s\n' "$*"; }
|
||||
|
||||
# ---- preconditions ----
|
||||
[ -n "${FORGEJO_TOKEN:-}" ] || die "FORGEJO_TOKEN not set — export it from Keychain via ~/.zshenv first."
|
||||
command -v python3 >/dev/null 2>&1 || die "python3 not found"
|
||||
command -v node >/dev/null 2>&1 || die "node not found"
|
||||
command -v curl >/dev/null 2>&1 || die "curl not found"
|
||||
[ -f "$MAP" ] || die "plugin-map.json missing at $MAP"
|
||||
[ -f "$LIVE" ] || die "live marketplace.json missing at $LIVE"
|
||||
BR="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
|
||||
[ "$BR" = "main" ] || die "catalog repo not on main (on '$BR')"
|
||||
|
||||
mapget() { python3 -c "import json;print(json.load(open('$MAP'))['targets']['$1'].get('$2',''))"; }
|
||||
|
||||
entry_present() {
|
||||
python3 -c "import json
|
||||
print('yes' if any(x['name']=='$1' for x in json.load(open('$LIVE'))['plugins']) else 'no')"
|
||||
}
|
||||
|
||||
entry_is_external() {
|
||||
python3 -c "import json
|
||||
p=[x for x in json.load(open('$LIVE'))['plugins'] if x['name']=='$1']
|
||||
print('yes' if (p and isinstance(p[0].get('source'),dict)) else 'no')"
|
||||
}
|
||||
|
||||
count_local() {
|
||||
python3 -c "import json
|
||||
print(sum(1 for x in json.load(open('$LIVE'))['plugins'] if isinstance(x.get('source'),str) and x['source'].startswith('./plugins/')))"
|
||||
}
|
||||
|
||||
rollout_one() {
|
||||
key="$1"
|
||||
tag="$(mapget "$key" tag)"
|
||||
[ -n "$tag" ] || die "no tag for $key in plugin-map.json"
|
||||
say ""
|
||||
say "==== $key (tag $tag) ===="
|
||||
|
||||
# (a) validate the extract — self-extracts if $WORK/$key is absent
|
||||
say " [a] validate extract…"
|
||||
if [ "$key" = "config-audit" ]; then
|
||||
WORK="$WORK" bash "$MIG/50-config-audit-sc2.sh" >/dev/null 2>&1 || die "$key SC2 gate FAILED — STOP"
|
||||
else
|
||||
WORK="$WORK" bash "$MIG/40-validate-standalone.sh" "$key" >/dev/null 2>&1 || die "$key standalone validation FAILED — STOP"
|
||||
fi
|
||||
|
||||
# (b) create the Forgejo repo (201 created | 409 already exists)
|
||||
say " [b] create repo open/$key…"
|
||||
body="$(mktemp)"
|
||||
code="$(curl -sS -o "$body" -w '%{http_code}' -X POST "$API/orgs/open/repos" \
|
||||
-H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
|
||||
-d "{\"name\":\"$key\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}")"
|
||||
case "$code" in
|
||||
201) say " created";;
|
||||
409) say " already exists (ok, idempotent)";;
|
||||
*) cat "$body" >&2; rm -f "$body"; die "$key repo-create unexpected HTTP $code — STOP";;
|
||||
esac
|
||||
rm -f "$body"
|
||||
|
||||
# (c) push the extract over SSH (proven auth; the marketplace source URL stays HTTPS)
|
||||
say " [c] push extract over SSH…"
|
||||
( cd "$WORK/$key" || exit 1
|
||||
git remote remove origin >/dev/null 2>&1 || true
|
||||
git remote add origin "ssh://git@$HOST/open/$key.git" || exit 1
|
||||
git push origin --all || exit 1
|
||||
git push origin --tags || exit 1
|
||||
) || die "$key push FAILED — STOP"
|
||||
|
||||
# (d) install-smoke PROXY: HTTPS clone at the pinned tag (the resolution path users hit)
|
||||
say " [d] verify HTTPS+ref resolution…"
|
||||
sm="$(mktemp -d)"
|
||||
git clone --quiet --branch "$tag" "https://$HOST/open/$key.git" "$sm/r" >/dev/null 2>&1 \
|
||||
|| die "$key does NOT resolve over HTTPS at $tag — STOP, diagnose the Forgejo/HTTPS/ref chain"
|
||||
if [ "$key" = "$DS_KEY" ]; then
|
||||
# design-system: no plugin.json — assert its DS root marker instead (tokens.css, per plugin-map test_cmd)
|
||||
[ -f "$sm/r/tokens.css" ] || die "$key clone missing tokens.css (DS root marker) — STOP"
|
||||
else
|
||||
[ -f "$sm/r/.claude-plugin/plugin.json" ] || die "$key clone missing .claude-plugin/plugin.json — STOP"
|
||||
fi
|
||||
say " resolves: https://$HOST/open/$key @ $tag ✓"
|
||||
|
||||
# (e) flip the catalog entry to the external nested source (idempotent) + push.
|
||||
# The design-system has no marketplace entry (consumers vendor it) — nothing to flip; this mirrors
|
||||
# 60-rewrite --all, which only touches the 10 plugins actually present in marketplace.json.
|
||||
if [ "$(entry_present "$key")" = "no" ]; then
|
||||
say " [e] $key has no marketplace entry (shared design-system, vendored by consumers) — no catalog flip"
|
||||
elif [ "$(entry_is_external "$key")" = "yes" ]; then
|
||||
say " [e] catalog already external for $key (skip)"
|
||||
else
|
||||
say " [e] flip catalog → external + push…"
|
||||
node "$MIG/60-rewrite-marketplace.mjs" --only "$key" --in "$LIVE" --out /tmp/mp-rollout.json >/dev/null \
|
||||
|| die "$key marketplace rewrite FAILED — STOP"
|
||||
cp /tmp/mp-rollout.json "$LIVE" || die "$key cp marketplace.json FAILED"
|
||||
git -C "$ROOT" add .claude-plugin/marketplace.json || die "$key git add FAILED"
|
||||
git -C "$ROOT" commit -q -m "chore(marketplace): externalise $key" || die "$key catalog commit FAILED"
|
||||
git -C "$ROOT" push origin main || die "$key catalog push FAILED — STOP"
|
||||
say " flipped + pushed"
|
||||
fi
|
||||
say " ✓ $key DONE"
|
||||
}
|
||||
|
||||
# ======================== §4 thinning ========================
|
||||
do_thin() {
|
||||
say "OPERATOR WINDOW — §4 thin catalog"
|
||||
locals="$(count_local)"
|
||||
[ "$locals" = "0" ] || die "$locals catalog entries still local — finish §0–§3 rollout before thinning"
|
||||
|
||||
ws="$(mktemp -d)"
|
||||
bash "$MIG/70-thin-catalog.sh" --workspace "$ws" >/dev/null || die "thin-catalog preview FAILED"
|
||||
[ ! -d "$ws/plugins" ] || die "thin preview still contains plugins/"
|
||||
[ ! -d "$ws/shared" ] || die "thin preview still contains shared/"
|
||||
[ -f "$ws/CONVENTIONS.md" ] || die "thin preview missing CONVENTIONS.md"
|
||||
say " thin preview verified at $ws (no plugins/ shared/, CONVENTIONS.md present)"
|
||||
|
||||
if [ "${CONFIRM_THIN:-}" != "1" ]; then
|
||||
say ""
|
||||
say " PREVIEW ONLY (irreversible apply is gated). To APPLY:"
|
||||
say " CONFIRM_THIN=1 bash $0 --thin"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
say " APPLYING irreversible thin state to the live catalog…"
|
||||
git -C "$ROOT" rm -r -q --ignore-unmatch plugins shared scripts/sync-design-system.mjs scripts/sync-design-system.test.mjs || true
|
||||
cp "$ws/CONVENTIONS.md" "$ROOT/CONVENTIONS.md" || die "cp CONVENTIONS.md FAILED"
|
||||
cp "$ws/CLAUDE.md" "$ROOT/CLAUDE.md" || die "cp CLAUDE.md FAILED"
|
||||
cp "$ws/README.md" "$ROOT/README.md" || die "cp README.md FAILED"
|
||||
git -C "$ROOT" add CONVENTIONS.md CLAUDE.md README.md || die "git add (thin docs) FAILED"
|
||||
git -C "$ROOT" add -A plugins shared scripts 2>/dev/null || true
|
||||
git -C "$ROOT" commit -q -m "chore(marketplace): thin catalog to manifest + docs (polyrepo migration complete)" \
|
||||
|| die "thin commit FAILED"
|
||||
git -C "$ROOT" push origin main || die "thin push FAILED"
|
||||
git -C "$ROOT" push origin "$ARCHIVE_TAG" || die "archive-tag push FAILED"
|
||||
say ""
|
||||
say "✓ THINNING COMPLETE — catalog is manifest + docs only; archive tag $ARCHIVE_TAG pushed."
|
||||
say " POLYREPO MIGRATION COMPLETE."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ======================== main ========================
|
||||
if [ "${1:-}" = "--thin" ]; then
|
||||
do_thin
|
||||
fi
|
||||
|
||||
say "OPERATOR WINDOW — §0–§3 rollout (create + push + flip; REVERSIBLE)"
|
||||
say "Targets in order: $PILOT (pilot) → $REST"
|
||||
say ""
|
||||
say "### PILOT: $PILOT — must pass before the other 10 ###"
|
||||
rollout_one "$PILOT"
|
||||
say ""
|
||||
say "### PILOT PASSED — rolling out the remaining 10 ###"
|
||||
for k in $REST; do rollout_one "$k"; done
|
||||
|
||||
say ""
|
||||
say "──────────────────────────────────────────────────────────────"
|
||||
say "✓ ROLLOUT COMPLETE — 11 repos created + pushed; marketplace.json all-external (nested HTTPS+ref)."
|
||||
say " Each plugin now lives at https://$HOST/open/<name> — clone & work on them in parallel."
|
||||
say ""
|
||||
say " NEXT (recommended before thinning) — real install-smoke per plugin in a FRESH Claude Code session:"
|
||||
say " /plugin marketplace update"
|
||||
say " /plugin install <name>@ktg-plugin-marketplace # confirm commands/skills/agents load"
|
||||
say " Reversible until thinning: 'git revert' the externalise commit and ./plugins/<name> resolves again."
|
||||
say ""
|
||||
say " When every install-smoke passes, run the irreversible cleanup:"
|
||||
say " CONFIRM_THIN=1 bash $0 --thin"
|
||||
Loading…
Add table
Add a link
Reference in a new issue