Files
reverse-engineering-anything/scripts/lib/cursor-plugin-bundle.mjs
T
a54e848302 feat(cursor): ship a Marketplace plugin with the skill and pinned MCP server (#9878)
Cursor Marketplace installs are Git checkouts, so the plugin manifest
points at a synced copy of the authored skill and starts
npx -y rea-agents@<release> mcp. Release Please and the release
checkpoint keep that pin aligned with the package version.

The manifest, skill, and icon checks follow the local plugin bundle
contract from #7619, adapted for a checkout that has no dist/.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: GamerDubz <105694977+GamerDubz@users.noreply.github.com>
2026-11-11 00:16:44 -05:00

217 lines
7.5 KiB
JavaScript

import { mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { ensureGeneratedFile } from "./generated-file.mjs";
// Identity fields follow the shared plugin manifest checked for local Cursor
// publication (name, release version, repository, skill, and icon). The
// Marketplace checkout has no dist/, so the server is the pinned npm command
// instead of the bundled launcher.
export const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json";
export const CURSOR_PLUGIN_MCP = "mcp.json";
export const AUTHORED_SKILL_DIRECTORY =
".agents/skills/reverse-engineer-anything";
export const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills";
export const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything";
export const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png";
export const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea";
const GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs";
const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"];
const failure = (code, detail) => {
const error = new Error(`${code}: ${detail}`);
error.code = code;
return error;
};
const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"];
/** Relative file inventory for one skill directory, in stable order. */
export const cursorPluginSkillPaths = async (directory, prefix = "") => {
const paths = [];
let entries;
try {
entries = await readdir(directory, { withFileTypes: true });
} catch (cause) {
if (cause?.code === "ENOENT") return null;
throw cause;
}
for (const entry of entries.sort((left, right) =>
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
)) {
const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
if (entry.isDirectory())
paths.push(
...(await cursorPluginSkillPaths(
join(directory, entry.name),
relativePath,
)),
);
else if (entry.isFile()) paths.push(relativePath);
else
throw failure(
"cursor_plugin_skill_inventory",
`Skill bundle requires regular files or directories: ${relativePath}`,
);
}
return paths;
};
const skillFrontmatter = (content) => {
const normalized = content.replaceAll("\r\n", "\n");
if (!normalized.startsWith("---\n")) return null;
const closing = normalized.indexOf("\n---\n", 4);
if (closing === -1) return null;
const fields = {};
for (const line of normalized.slice(4, closing).split("\n")) {
const separator = line.indexOf(":");
if (separator === -1) continue;
const key = line.slice(0, separator).trim();
const value = line.slice(separator + 1).trim();
if (key !== "" && fields[key] === undefined) fields[key] = value;
}
return fields;
};
/**
* Structured differences between the Cursor plugin declaration and the
* package version it must launch. Empty means the manifest, server pin,
* logo, and skill frontmatter agree.
*/
export const cursorPluginBundleIssues = ({
manifest,
mcp,
packageVersion,
logoExists,
skillNames,
skillFrontmatter: frontmatter,
}) => {
const issues = [];
const add = (field) => issues.push({ field });
if (manifest?.name !== "rea") add("name");
if (manifest?.version !== packageVersion) add("version");
if (typeof manifest?.description !== "string" || manifest.description === "")
add("description");
if (typeof manifest?.author?.name !== "string" || manifest.author.name === "")
add("author.name");
if (manifest?.repository !== CURSOR_PLUGIN_REPOSITORY) add("repository");
if (typeof manifest?.license !== "string" || manifest.license === "")
add("license");
if (
!Array.isArray(manifest?.keywords) ||
manifest.keywords.length === 0 ||
manifest.keywords.some((keyword) => typeof keyword !== "string")
)
add("keywords");
if (manifest?.skills !== CURSOR_PLUGIN_SKILLS) add("skills");
if (manifest?.logo !== CURSOR_PLUGIN_LOGO) add("logo");
if (Object.hasOwn(manifest ?? {}, "mcpServers")) add("mcpServers");
if (logoExists !== true) add("logo.file");
const server = mcp?.mcpServers?.rea;
if (
server?.command !== "npx" ||
JSON.stringify(server?.args) !==
JSON.stringify(cursorServerArgs(packageVersion))
)
add("mcp.args");
if (
!Array.isArray(skillNames) ||
skillNames.length !== 1 ||
skillNames[0] !== CURSOR_PLUGIN_SKILL
)
add("skills.entries");
if (frontmatter?.name !== CURSOR_PLUGIN_SKILL) add("skill.name");
if (
typeof frontmatter?.description !== "string" ||
frontmatter.description === ""
)
add("skill.description");
return issues;
};
/** Copy the authored skill into the Marketplace plugin tree, or fail when it drifted. */
export const syncCursorPluginSkill = async ({ root, check }) => {
const sourceRoot = join(root, AUTHORED_SKILL_DIRECTORY);
const outputRoot = join(root, CURSOR_PLUGIN_SKILLS, CURSOR_PLUGIN_SKILL);
const sourcePaths = await cursorPluginSkillPaths(sourceRoot);
if (sourcePaths === null)
throw failure(
"cursor_plugin_skill_inventory",
`Authored skill is missing: ${AUTHORED_SKILL_DIRECTORY}`,
);
if (!check) await rm(outputRoot, { recursive: true, force: true });
for (const relativePath of sourcePaths) {
const source = (
await readFile(join(sourceRoot, relativePath), "utf8")
).replaceAll("\r\n", "\n");
if (!check)
await mkdir(dirname(join(outputRoot, relativePath)), { recursive: true });
try {
await ensureGeneratedFile({
path: join(outputRoot, relativePath),
source,
check,
generateCommand: GENERATE_COMMAND,
});
} catch (cause) {
throw failure(
"cursor_plugin_skill_stale",
cause instanceof Error ? cause.message : String(cause),
);
}
}
const outputPaths = await cursorPluginSkillPaths(outputRoot);
if (
outputPaths === null ||
JSON.stringify(outputPaths) !== JSON.stringify(sourcePaths)
)
throw failure(
"cursor_plugin_skill_inventory",
"Cursor plugin skill inventory drifted; run node scripts/generate-cursor-plugin-skill.mjs",
);
};
/** Read the committed plugin and report structured contract issues. */
export const inspectCursorPluginBundle = async (root) => {
const manifest = JSON.parse(
await readFile(join(root, CURSOR_PLUGIN_MANIFEST), "utf8"),
);
const mcp = JSON.parse(await readFile(join(root, CURSOR_PLUGIN_MCP), "utf8"));
const packageJson = JSON.parse(
await readFile(join(root, "package.json"), "utf8"),
);
const logoExists = await stat(join(root, CURSOR_PLUGIN_LOGO))
.then((metadata) => metadata.isFile())
.catch((cause) => {
if (cause?.code === "ENOENT") return false;
throw cause;
});
const skillDirectory = join(root, CURSOR_PLUGIN_SKILLS);
const skillNames = await readdir(skillDirectory);
const skillContent = await readFile(
join(skillDirectory, CURSOR_PLUGIN_SKILL, "SKILL.md"),
"utf8",
);
return {
manifest,
mcp,
packageVersion: packageJson.version,
issues: cursorPluginBundleIssues({
manifest,
mcp,
packageVersion: packageJson.version,
logoExists,
skillNames,
skillFrontmatter: skillFrontmatter(skillContent),
}),
};
};
/** MCP-only install links omit the skill, so Marketplace docs must not carry them. */
export const cursorMarketplaceInstallLinkIssues = (documents) =>
documents.flatMap(({ path, text }) =>
INSTALL_LINK_MARKERS.filter((marker) => text.includes(marker)).map(
(marker) => ({ path, marker }),
),
);