ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/run-operator-window.sh
Kjell Tore Guttormsen ebd0a9c617 docs(migration): brace ${key} in run-operator-window — bash 5.3 absorbed the ellipsis byte into the var name under set -u
The pilot died at line 93 with 'key<byte>: unbound variable': $key glued to
the U+2026 ellipsis let bash 5.3.9 read the multibyte byte as part of the name.
Braced to ${key}…; proven on /usr/local/bin/bash (repro + fixed form + bash -n).
Nothing was mutated on Forgejo — it failed before the [b] repo-create POST.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 05:49:47 +02:00

209 lines
11 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"