chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:11:34 +08:00
commit f20bdd32f3
408 changed files with 77098 additions and 0 deletions
+229
View File
@@ -0,0 +1,229 @@
#!/usr/bin/env python3
"""Discover plugins in marketplace.json whose upstream repo has moved past
their pinned SHA, update the file in place, and emit a summary.
Adapted from claude-plugins-community-internal's discover_bumps.py for the
single-file marketplace.json format used by claude-plugins-official.
Usage: discover_bumps.py [--plugin NAME] [--max N] [--dry-run]
"""
import argparse
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from typing import Any
MARKETPLACE_PATH = ".claude-plugin/marketplace.json"
def gh_api(path: str) -> Any:
"""GET from the GitHub API. None on not-found; raises on other errors.
"Not found" covers both 404 (resource gone) and 422 "No commit found
for SHA" (force-pushed away). Both mean the thing we asked for isn't
there — treating them the same lets callers handle dead refs uniformly.
"""
r = subprocess.run(
["gh", "api", path], capture_output=True, text=True
)
if r.returncode != 0:
combined = r.stdout + r.stderr
if any(s in combined for s in ("404", "Not Found", "No commit found")):
return None
raise RuntimeError(f"gh api {path}: {r.stderr.strip() or r.stdout.strip()}")
return json.loads(r.stdout)
def parse_github_repo(url: str) -> tuple[str, str] | None:
"""Extract (owner, repo) from a URL or owner/repo shorthand."""
# Full URL: https://github.com/owner/repo(.git)(/...)
m = re.match(r"https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?(?:/|$)", url)
if m:
return m.group(1), m.group(2)
# Shorthand: owner/repo
m = re.match(r"^([\w.-]+)/([\w.-]+)$", url)
if m:
return m.group(1), m.group(2)
return None
def latest_sha(owner: str, repo: str, *, ref: str | None, path: str | None) -> str | None:
"""Latest commit SHA for the repo, optionally scoped to a ref and/or path."""
if path:
# Scoped to a subdirectory — use the commits list endpoint with path filter.
q = f"repos/{owner}/{repo}/commits?per_page=1&path={path}"
if ref:
q += f"&sha={ref}"
commits = gh_api(q)
if not commits:
return None
return commits[0]["sha"]
# Whole repo — the single-ref endpoint is cheaper.
if not ref:
meta = gh_api(f"repos/{owner}/{repo}")
if not meta:
return None
ref = meta["default_branch"]
c = gh_api(f"repos/{owner}/{repo}/commits/{ref}")
return c["sha"] if c else None
def pinned_age_days(owner: str, repo: str, sha: str) -> int | None:
"""Days since the pinned commit was authored. Used for oldest-first rotation."""
c = gh_api(f"repos/{owner}/{repo}/commits/{sha}")
if not c:
return None
dt = datetime.fromisoformat(
c["commit"]["committer"]["date"].replace("Z", "+00:00")
)
return (datetime.now(timezone.utc) - dt).days
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--plugin", help="only check this plugin")
ap.add_argument("--max", type=int, default=20, help="cap bumps emitted")
ap.add_argument("--dry-run", action="store_true", help="don't write marketplace.json")
args = ap.parse_args()
with open(MARKETPLACE_PATH) as f:
marketplace = json.load(f)
plugins = marketplace.get("plugins", [])
bumps: list[dict] = []
dead: list[str] = []
skipped_non_github = 0
checked = 0
for plugin in plugins:
name = plugin.get("name", "?")
src = plugin.get("source")
# Only process object sources with a sha field
if not isinstance(src, dict) or "sha" not in src:
continue
# Filter to specific plugin if requested
if args.plugin and name != args.plugin:
continue
checked += 1
kind = src.get("source")
url = src.get("url", "")
path = src.get("path")
ref = src.get("ref")
pinned = src.get("sha")
slug = parse_github_repo(url)
if not slug:
skipped_non_github += 1
continue
owner, repo = slug
try:
latest = latest_sha(owner, repo, ref=ref, path=path)
except RuntimeError as e:
print(f"::warning::{name}: {e}", file=sys.stderr)
continue
if latest is None:
dead.append(f"{name} ({owner}/{repo})")
continue
if latest == pinned:
continue # up to date
# Age lookup for rotation — oldest-pinned first prevents starvation.
try:
age = pinned_age_days(owner, repo, pinned) if pinned else None
except RuntimeError as e:
print(f"::warning::{name}: age lookup failed: {e}", file=sys.stderr)
age = None
bumps.append({
"name": name,
"kind": kind,
"url": url,
"path": path or "",
"ref": ref or "",
"old_sha": pinned or "",
"new_sha": latest,
"age_days": age if age is not None else 10**6,
})
# Oldest-pinned first so nothing starves under the cap.
bumps.sort(key=lambda b: -b["age_days"])
emitted = bumps[: args.max]
# Apply bumps to marketplace data
if emitted and not args.dry_run:
bump_map = {b["name"]: b["new_sha"] for b in emitted}
for plugin in plugins:
name = plugin.get("name")
src = plugin.get("source")
if isinstance(src, dict) and name in bump_map:
src["sha"] = bump_map[name]
with open(MARKETPLACE_PATH, "w") as f:
json.dump(marketplace, f, indent=2, ensure_ascii=False)
f.write("\n")
# Write GitHub outputs
out = os.environ.get("GITHUB_OUTPUT")
if out:
bumped_names = ",".join(b["name"] for b in emitted)
with open(out, "a") as fh:
fh.write(f"count={len(emitted)}\n")
fh.write(f"bumped_names={bumped_names}\n")
# Write GitHub step summary
summary = os.environ.get("GITHUB_STEP_SUMMARY")
if summary:
with open(summary, "a") as fh:
fh.write("## SHA Bump Discovery\n\n")
fh.write(f"- Checked: {checked} SHA-pinned entries\n")
fh.write(f"- Stale: {len(bumps)} (applying {len(emitted)}, cap {args.max})\n")
if skipped_non_github:
fh.write(f"- Skipped non-GitHub: {skipped_non_github}\n")
if dead:
fh.write(f"- **Dead upstream** ({len(dead)}): {', '.join(dead)}\n")
if emitted:
fh.write("\n| Plugin | Old | New | Age |\n|---|---|---|---|\n")
for b in emitted:
old = b["old_sha"][:8] if b["old_sha"] else "(unpinned)"
fh.write(f"| {b['name']} | `{old}` | `{b['new_sha'][:8]}` | {b['age_days']}d |\n")
# Write PR body for the workflow to use
pr_body_path = os.environ.get("PR_BODY_PATH", "/tmp/bump-pr-body.md")
if emitted:
with open(pr_body_path, "w") as fh:
fh.write("Upstream repos moved. Bumping pinned SHAs so plugins track latest.\n\n")
fh.write("| Plugin | Old | New | Upstream |\n")
fh.write("|--------|-----|-----|----------|\n")
for b in emitted:
old = b["old_sha"][:8] if b["old_sha"] else "(unpinned)"
slug_str = re.sub(r"https?://github\.com/", "", b["url"])
slug_str = re.sub(r"\.git$", "", slug_str)
compare = f"https://github.com/{slug_str}/compare/{b['old_sha'][:12]}...{b['new_sha'][:12]}"
fh.write(f"| `{b['name']}` | `{old}` | `{b['new_sha'][:8]}` | [diff]({compare}) |\n")
fh.write(f"\n---\n_Auto-generated by `bump-plugin-shas.yml` on {datetime.now(timezone.utc).strftime('%Y-%m-%d')}_\n")
# Console summary
print(f"Checked {checked} SHA-pinned plugins", file=sys.stderr)
print(f"Stale: {len(bumps)}, applying: {len(emitted)}", file=sys.stderr)
if dead:
print(f"Dead upstream: {', '.join(dead)}", file=sys.stderr)
for b in emitted:
old = b["old_sha"][:8] if b["old_sha"] else "unpinned"
print(f" {b['name']}: {old} -> {b['new_sha'][:8]} ({b['age_days']}d)", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
+153
View File
@@ -0,0 +1,153 @@
'use strict';
// Shared logic for letting a NON-MEMBER pull request stay open and be reviewed, scoped to
// the contributor's own already-listed plugin repo. No maintained allowlist, no individuals.
//
// Trust model: we do NOT verify the submitter's identity. We trust the SOURCE REPO. A PR is
// in scope only if it ADDS marketplace.json entries whose source.url is a repo that ALREADY
// backs a live entry in this marketplace (derived from the base marketplace.json), pinned to
// a commit in that repo. Because the repo is org-controlled and the SHA pins to a real commit
// there, the shipped code is the org's code regardless of who opened the PR. Merge still
// requires CI + a maintainer approval.
//
// Used by:
// - close-external-prs.yml (skip the auto-close when in scope)
// - external-pr-scope-guard.yml (required status check: fail a non-member PR that is out of scope)
//
// Security: evaluate() reads base + head marketplace.json as DATA via the API and parses them;
// it never checks out or executes head code.
const MARKETPLACE = '.claude-plugin/marketplace.json';
function normalizeRepo(u) {
return String(u || '').trim().toLowerCase()
.replace(/^git\+/, '')
.replace(/^https?:\/\//, '')
.replace(/\.git$/, '')
.replace(/\/+$/, '');
}
function pluginsByName(json) {
const map = {};
for (const p of (json && json.plugins) || []) { if (p && p.name) map[p.name] = p; }
return map;
}
// Repos that already back a live entry, derived from the base marketplace.json.
function liveReposOf(base) {
const s = new Set();
for (const name of Object.keys(base)) {
const u = base[name] && base[name].source && base[name].source.url;
if (!u) continue;
const r = normalizeRepo(u);
if (r.split('/').length >= 3) s.add(r); // host/org/repo
}
return s;
}
// Pure decision over an already-computed diff. Returns { ok, problems, added, removed, modified }.
// before = plugins at the MERGE-BASE (what head forked from), after = plugins at HEAD,
// liveRepos = repos already live on the current base branch. Diffing before->after (not
// base-tip->head) isolates THIS PR's changes; a stale fork no longer shows main's later
// additions as phantom removals.
function analyze({ changedFiles, before, after, liveRepos }) {
const problems = [];
const off = changedFiles.filter(n => n !== MARKETPLACE);
if (off.length) problems.push(`changes files other than ${MARKETPLACE}: ${off.join(', ')}`);
const baseNames = new Set(Object.keys(before));
const headNames = new Set(Object.keys(after));
const removed = [...baseNames].filter(n => !headNames.has(n));
const added = [...headNames].filter(n => !baseNames.has(n));
const modified = [...headNames].filter(
n => baseNames.has(n) && JSON.stringify(before[n]) !== JSON.stringify(after[n])
);
if (removed.length) problems.push(`removes existing entr${removed.length > 1 ? 'ies' : 'y'}: ${removed.join(', ')}`);
if (modified.length) problems.push(`modifies existing entr${modified.length > 1 ? 'ies' : 'y'}: ${modified.join(', ')}`);
if (!off.length && !added.length && !removed.length && !modified.length) {
problems.push('makes no in-scope change (expected additions to marketplace.json)');
}
for (const name of added) {
const u = after[name] && after[name].source && after[name].source.url;
if (!u) { problems.push(`added "${name}" has no source.url to validate`); continue; }
const r = normalizeRepo(u);
if (r.split('/').length < 3) { problems.push(`added "${name}" source.url ${u} is not a valid repo URL`); continue; }
if (!liveRepos.has(r)) {
problems.push(`added "${name}" points at ${u}, a repo with no existing live plugin in this marketplace`);
}
}
return { ok: problems.length === 0, problems, added, removed, modified, liveRepoCount: liveRepos.size };
}
async function readPlugins(github, owner, repo, ref) {
try {
const { data } = await github.rest.repos.getContent({ owner, repo, ref, path: MARKETPLACE });
return pluginsByName(JSON.parse(Buffer.from(data.content, 'base64').toString('utf8')));
} catch (e) {
return null;
}
}
// API wrapper used by both workflows. Fetches the diff + base/head marketplace.json, delegates to analyze().
async function evaluate({ github, context }) {
const pr = context.payload.pull_request;
const owner = context.repo.owner, repo = context.repo.repo;
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100,
});
const changedFiles = files.map(f => f.filename);
// Diff THIS PR's changes (merge-base -> head), not base-tip -> head, so a fork that is
// behind main doesn't show main's later additions as phantom removals.
let mergeBaseSha = pr.base.sha;
try {
const cmp = await github.rest.repos.compareCommits({ owner, repo, base: pr.base.sha, head: pr.head.sha });
if (cmp && cmp.data && cmp.data.merge_base_commit && cmp.data.merge_base_commit.sha) {
mergeBaseSha = cmp.data.merge_base_commit.sha;
}
} catch (e) { /* fall back to base.sha */ }
const liveBase = await readPlugins(github, owner, repo, pr.base.sha); // current base branch (for "already live")
const before = await readPlugins(github, owner, repo, mergeBaseSha); // what head forked from
const after = await readPlugins(github, pr.head.repo.owner.login, pr.head.repo.name, pr.head.sha);
if (liveBase === null || before === null || after === null) {
return { ok: false, problems: ['could not read marketplace.json at base, merge-base, and/or head'], added: [], removed: [], modified: [] };
}
return analyze({ changedFiles, before, after, liveRepos: liveReposOf(liveBase) });
}
// Authors that are NOT subject to the external-contributor scope rules:
// - the repo's own automation bot — its bump PRs legitimately MODIFY existing entries
// (SHA bumps), which the additions-only external-contributor rule forbids; AND
// - org members (write/admin).
// Safe under pull_request_target: a fork PR cannot set its author to github-actions[bot]
// (that login is only ever the org's own GITHUB_TOKEN workflow), and the member path is a
// real permission lookup. Wrapped in try/catch because getCollaboratorPermissionLevel throws
// for a non-collaborator/unknown user — without this, both callers would error the job rather
// than fall through to scope evaluation.
const EXEMPT_BOTS = new Set(['github-actions[bot]']);
async function isExemptAuthor({ github, context }) {
const author = context.payload.pull_request.user.login;
if (EXEMPT_BOTS.has(author)) {
return { exempt: true, reason: `${author} is the trusted automation bot` };
}
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner: context.repo.owner, repo: context.repo.repo, username: author,
});
if (['admin', 'write'].includes(data.permission)) {
return { exempt: true, reason: `${author} is ${data.permission} (member)` };
}
} catch (e) {
// not a collaborator / lookup failed → not exempt; fall through to scope evaluation
}
return { exempt: false };
}
module.exports = { normalizeRepo, liveReposOf, analyze, readPlugins, evaluate, isExemptAuthor, MARKETPLACE };
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env bun
/**
* Validates YAML frontmatter in agent, skill, and command .md files.
*
* Usage:
* bun validate-frontmatter.ts # scan current directory
* bun validate-frontmatter.ts /path/to/dir # scan specific directory
* bun validate-frontmatter.ts file1.md file2.md # validate specific files
*/
import { parse as parseYaml } from "yaml";
import { readdir, readFile } from "fs/promises";
import { basename, join, relative, resolve } from "path";
// Characters that require quoting in YAML values when unquoted:
// {} [] flow indicators, * anchor/alias, & anchor, # comment,
// ! tag, | > block scalars, % directive, @ ` reserved
const YAML_SPECIAL_CHARS = /[{}[\]*&#!|>%@`]/;
const FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)---\s*\n?/;
/**
* Pre-process frontmatter text to quote values containing special YAML
* characters. This allows glob patterns like **\/*.{ts,tsx} to parse.
*/
function quoteSpecialValues(text: string): string {
const lines = text.split("\n");
const result: string[] = [];
for (const line of lines) {
const match = line.match(/^([a-zA-Z_-]+):\s+(.+)$/);
if (match) {
const [, key, value] = match;
if (!key || !value) {
result.push(line);
continue;
}
// Skip already-quoted values
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
result.push(line);
continue;
}
if (YAML_SPECIAL_CHARS.test(value)) {
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
result.push(`${key}: "${escaped}"`);
continue;
}
}
result.push(line);
}
return result.join("\n");
}
interface ParseResult {
frontmatter: Record<string, unknown>;
content: string;
error?: string;
}
function parseFrontmatter(markdown: string): ParseResult {
const match = markdown.match(FRONTMATTER_REGEX);
if (!match) {
return {
frontmatter: {},
content: markdown,
error: "No frontmatter found",
};
}
const frontmatterText = quoteSpecialValues(match[1] || "");
const content = markdown.slice(match[0].length);
try {
const parsed = parseYaml(frontmatterText);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return { frontmatter: parsed as Record<string, unknown>, content };
}
return {
frontmatter: {},
content,
error: `YAML parsed but result is not an object (got ${typeof parsed}${Array.isArray(parsed) ? " array" : ""})`,
};
} catch (err) {
return {
frontmatter: {},
content,
error: `YAML parse failed: ${err instanceof Error ? err.message : err}`,
};
}
}
// --- Validation ---
type FileType = "agent" | "skill" | "command";
interface ValidationIssue {
level: "error" | "warning";
message: string;
}
function validateAgent(
frontmatter: Record<string, unknown>
): ValidationIssue[] {
const issues: ValidationIssue[] = [];
if (!frontmatter["name"] || typeof frontmatter["name"] !== "string") {
issues.push({ level: "error", message: 'Missing required "name" field' });
}
if (
!frontmatter["description"] ||
typeof frontmatter["description"] !== "string"
) {
issues.push({
level: "error",
message: 'Missing required "description" field',
});
}
return issues;
}
function validateSkill(
frontmatter: Record<string, unknown>
): ValidationIssue[] {
const issues: ValidationIssue[] = [];
if (!frontmatter["description"] && !frontmatter["when_to_use"]) {
issues.push({
level: "error",
message: 'Missing required "description" field',
});
}
return issues;
}
function validateCommand(
frontmatter: Record<string, unknown>
): ValidationIssue[] {
const issues: ValidationIssue[] = [];
if (
!frontmatter["description"] ||
typeof frontmatter["description"] !== "string"
) {
issues.push({
level: "error",
message: 'Missing required "description" field',
});
}
return issues;
}
// --- File type detection ---
function detectFileType(filePath: string): FileType | null {
// Only match agents/ and commands/ at the plugin root level, not nested
// inside skill content (e.g. plugins/foo/skills/bar/agents/ is skill content,
// not an agent definition).
const inSkillContent = /\/skills\/[^/]+\//.test(filePath);
if (filePath.includes("/agents/") && !inSkillContent) return "agent";
if (filePath.includes("/skills/") && basename(filePath) === "SKILL.md")
return "skill";
if (filePath.includes("/commands/") && !inSkillContent) return "command";
return null;
}
// --- File discovery ---
async function findMdFiles(
baseDir: string
): Promise<{ path: string; type: FileType }[]> {
const results: { path: string; type: FileType }[] = [];
async function walk(dir: string) {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
await walk(fullPath);
} else if (entry.name.endsWith(".md")) {
const type = detectFileType(fullPath);
if (type) {
results.push({ path: fullPath, type });
}
}
}
}
await walk(baseDir);
return results;
}
// --- Main ---
async function main() {
const args = process.argv.slice(2);
let files: { path: string; type: FileType }[];
let baseDir: string;
if (args.length > 0 && args.every((a) => a.endsWith(".md"))) {
baseDir = process.cwd();
files = [];
for (const arg of args) {
const fullPath = resolve(arg);
const type = detectFileType(fullPath);
if (type) {
files.push({ path: fullPath, type });
}
}
} else {
baseDir = args[0] || process.cwd();
files = await findMdFiles(baseDir);
}
let totalErrors = 0;
let totalWarnings = 0;
console.log(`Validating ${files.length} frontmatter files...\n`);
for (const { path: filePath, type } of files) {
const rel = relative(baseDir, filePath);
const content = await readFile(filePath, "utf-8");
const result = parseFrontmatter(content);
const issues: ValidationIssue[] = [];
if (result.error) {
issues.push({ level: "error", message: result.error });
}
if (!result.error) {
switch (type) {
case "agent":
issues.push(...validateAgent(result.frontmatter));
break;
case "skill":
issues.push(...validateSkill(result.frontmatter));
break;
case "command":
issues.push(...validateCommand(result.frontmatter));
break;
}
}
if (issues.length > 0) {
console.log(`${rel} (${type})`);
for (const issue of issues) {
const prefix = issue.level === "error" ? " ERROR" : " WARN ";
console.log(`${prefix}: ${issue.message}`);
if (issue.level === "error") totalErrors++;
else totalWarnings++;
}
console.log();
}
}
console.log("---");
console.log(
`Validated ${files.length} files: ${totalErrors} errors, ${totalWarnings} warnings`
);
if (totalErrors > 0) {
process.exit(1);
}
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(2);
});