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