chore(migration): preflight script + rename-aware plugin map
This commit is contained in:
parent
a44e37b71f
commit
f4c36fcda5
3 changed files with 329 additions and 0 deletions
134
docs/marketplace-polyrepo-migration/migration/00-preflight.sh
Normal file
134
docs/marketplace-polyrepo-migration/migration/00-preflight.sh
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#!/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"
|
||||
|
||||
# --- 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"
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
// Step 1 test — validates plugin-map.json shape (rename-awareness, HTTPS URLs, blob/vendor flags).
|
||||
// Pattern: plugins/voyage/tests/validators/*.test.mjs (node:test style, zero deps).
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const map = JSON.parse(readFileSync(join(here, 'plugin-map.json'), 'utf8'));
|
||||
const targets = map.targets;
|
||||
const keys = Object.keys(targets);
|
||||
|
||||
const RENAMED = ['voyage', 'llm-security', 'linkedin-studio'];
|
||||
|
||||
test('plugin-map.json parses and has exactly 11 targets (10 plugins + DS)', () => {
|
||||
assert.equal(keys.length, 11, `expected 11 targets, found ${keys.length}: ${keys.join(', ')}`);
|
||||
});
|
||||
|
||||
test('the 3 renamed plugins each carry >=2 --path entries (F1)', () => {
|
||||
for (const k of RENAMED) {
|
||||
assert.ok(targets[k], `missing renamed target ${k}`);
|
||||
assert.ok(
|
||||
Array.isArray(targets[k].paths) && targets[k].paths.length >= 2,
|
||||
`${k} must carry >=2 paths (both names), has ${targets[k].paths?.length}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('every target repo_url is https:// (not ssh://) — F2 / #9740', () => {
|
||||
for (const k of keys) {
|
||||
assert.match(targets[k].repo_url, /^https:\/\/git\.fromaitochitta\.com\/open\//, `${k} repo_url not the expected HTTPS form`);
|
||||
assert.doesNotMatch(targets[k].repo_url, /^ssh:\/\//, `${k} repo_url must not be ssh://`);
|
||||
}
|
||||
});
|
||||
|
||||
test('ms-ai-architect has blob_strip: true (F3)', () => {
|
||||
assert.equal(targets['ms-ai-architect'].blob_strip, true);
|
||||
});
|
||||
|
||||
test('only llm-security + ms-ai-architect have has_vendor_ds: true', () => {
|
||||
const vendor = keys.filter((k) => targets[k].has_vendor_ds === true).sort();
|
||||
assert.deepEqual(vendor, ['llm-security', 'ms-ai-architect']);
|
||||
});
|
||||
|
||||
test('every target tag is v<semver> (F5 single clean re-tag per repo)', () => {
|
||||
for (const k of keys) {
|
||||
assert.match(targets[k].tag, /^v\d+\.\d+\.\d+$/, `${k} tag '${targets[k].tag}' is not v<semver>`);
|
||||
}
|
||||
});
|
||||
|
||||
test('the removed plugin is dropped, not extracted', () => {
|
||||
assert.ok(Array.isArray(map.drop) && map.drop.includes('plugins/ultra-cc-architect/'));
|
||||
assert.ok(!keys.includes('ultra-cc-architect'));
|
||||
});
|
||||
140
docs/marketplace-polyrepo-migration/migration/plugin-map.json
Normal file
140
docs/marketplace-polyrepo-migration/migration/plugin-map.json
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
"_comment": "Rename-aware extraction map for the polyrepo migration. Tags come from each plugin's .claude-plugin/plugin.json version (verified 2026-06-17), NOT from README/CLAUDE.md (which the migration corrects, Step 9). 11 targets = 10 plugins + the design-system. URLs are HTTPS (F2, #9740). Renamed plugins carry >=2 paths (F1). blob_strip_safe is (re)computed by 00-preflight.sh against the mirror history.",
|
||||
"drop": ["plugins/ultra-cc-architect/"],
|
||||
"targets": {
|
||||
"ai-psychosis": {
|
||||
"paths": ["plugins/ai-psychosis/"],
|
||||
"path_renames": { "plugins/ai-psychosis/": "" },
|
||||
"tag": "v1.2.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/ai-psychosis.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "node --test 'tests/**/*.test.mjs'",
|
||||
"standalone_caveat": "runs from repo root (relative-fixture cwd)"
|
||||
},
|
||||
"claude-design": {
|
||||
"paths": ["plugins/claude-design/"],
|
||||
"path_renames": { "plugins/claude-design/": "" },
|
||||
"tag": "v0.1.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/claude-design.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "bash tests/validate-plugin.sh",
|
||||
"standalone_caveat": "structure check via validate-plugin.sh (no unit suite)"
|
||||
},
|
||||
"config-audit": {
|
||||
"paths": ["plugins/config-audit/"],
|
||||
"path_renames": { "plugins/config-audit/": "" },
|
||||
"tag": "v5.1.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/config-audit.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "node --test 'tests/**/*.test.mjs'",
|
||||
"standalone_caveat": "SC2 gate = full tests/**/*.test.mjs MINUS json-backcompat + raw-backcompat (frozen back-compat snapshots embed the original machine's absolute path + sibling marketplace + deleted plugins); re-seed snapshot-default-output via UPDATE_SNAPSHOT=1 in-clone (Step 7)"
|
||||
},
|
||||
"graceful-handoff": {
|
||||
"paths": ["plugins/graceful-handoff/"],
|
||||
"path_renames": { "plugins/graceful-handoff/": "" },
|
||||
"tag": "v2.1.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/graceful-handoff.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "node --test 'tests/**/*.test.mjs'",
|
||||
"standalone_caveat": "pilot candidate (low-churn, has tests, no DS, no blob bloat)"
|
||||
},
|
||||
"human-friendly-style": {
|
||||
"paths": ["plugins/human-friendly-style/"],
|
||||
"path_renames": { "plugins/human-friendly-style/": "" },
|
||||
"tag": "v1.1.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/human-friendly-style.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "bash validate-plugin.generic.sh human-friendly-style",
|
||||
"standalone_caveat": "no unit suite — ported generic structure validator (Step 7)"
|
||||
},
|
||||
"linkedin-studio": {
|
||||
"paths": ["plugins/linkedin-studio/", "plugins/linkedin-thought-leadership/"],
|
||||
"path_renames": {
|
||||
"plugins/linkedin-studio/": "",
|
||||
"plugins/linkedin-thought-leadership/": ""
|
||||
},
|
||||
"tag": "v0.4.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/linkedin-studio.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "node --test hooks/scripts/__tests__/*.test.mjs render/__tests__/*.test.mjs agents/__tests__/*.test.mjs",
|
||||
"test_cmd_advisory": "cd scripts/analytics && npm install && npm test",
|
||||
"standalone_caveat": "renamed from linkedin-thought-leadership (F1). SC2 two-tier (M11): .mjs core is the HARD gate (zero deps); TS analytics suite is ADVISORY (npm/network required, NOT a clean-room gate)"
|
||||
},
|
||||
"llm-security": {
|
||||
"paths": ["plugins/llm-security/", "plugins/llm-security-copilot/"],
|
||||
"path_renames": {
|
||||
"plugins/llm-security/": "",
|
||||
"plugins/llm-security-copilot/": ""
|
||||
},
|
||||
"tag": "v7.7.2",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/llm-security.git",
|
||||
"has_vendor_ds": true,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "node --test 'tests/**/*.test.mjs'",
|
||||
"standalone_caveat": "renamed from llm-security-copilot (F1); vendors the design-system (playground)"
|
||||
},
|
||||
"ms-ai-architect": {
|
||||
"paths": ["plugins/ms-ai-architect/"],
|
||||
"path_renames": { "plugins/ms-ai-architect/": "" },
|
||||
"tag": "v1.15.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/ms-ai-architect.git",
|
||||
"has_vendor_ds": true,
|
||||
"blob_strip": true,
|
||||
"blob_strip_safe": true,
|
||||
"test_cmd": "bash tests/validate-plugin.sh",
|
||||
"standalone_caveat": "148 MB screenshot blob-bomb (F3): single filter-repo pass strips >1MB blobs when blob_strip_safe (all under playground/screenshots/), else surgical --path-glob; vendors the design-system"
|
||||
},
|
||||
"okr": {
|
||||
"paths": ["plugins/okr/"],
|
||||
"path_renames": { "plugins/okr/": "" },
|
||||
"tag": "v1.3.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/okr.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "bash validate-plugin.generic.sh okr",
|
||||
"standalone_caveat": "no unit suite — ported generic structure validator (Step 7). okr/templates/okr.local.md.template is intentionally tracked (SC7 allow-exception)"
|
||||
},
|
||||
"voyage": {
|
||||
"paths": ["plugins/voyage/", "plugins/ultraplan-local/"],
|
||||
"path_renames": {
|
||||
"plugins/voyage/": "",
|
||||
"plugins/ultraplan-local/": ""
|
||||
},
|
||||
"tag": "v5.1.1",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/voyage.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "node --test 'tests/**/*.test.mjs'",
|
||||
"standalone_caveat": "renamed from ultraplan-local (F1, 136 pre-rename commits). Carries .forgejo/ISSUE_TEMPLATE/ under --path — assert it survives, no monorepo-relative refs (M17)"
|
||||
},
|
||||
"playground-design-system": {
|
||||
"paths": ["shared/playground-design-system/", "shared/playground-examples/"],
|
||||
"path_renames": {
|
||||
"shared/playground-design-system/": "",
|
||||
"shared/playground-examples/": "playground-examples/"
|
||||
},
|
||||
"tag": "v0.6.0",
|
||||
"repo_url": "https://git.fromaitochitta.com/open/playground-design-system.git",
|
||||
"has_vendor_ds": false,
|
||||
"blob_strip": false,
|
||||
"blob_strip_safe": null,
|
||||
"test_cmd": "bash -c 'test -f tokens.css && test -f base.css && test -d schemas'",
|
||||
"standalone_caveat": "DS source repo (the 2 consumers are llm-security + ms-ai-architect). Receives sync-design-system.mjs + test + PLAYGROUND-MAINTENANCE.md at standup (Step 4)"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue