mirror of
https://github.com/MAKS-IT-COM/maksit-gitea-cursor.git
synced 2026-09-29 20:48:10 +02:00
175 lines
6.2 KiB
JavaScript
175 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
const root = process.cwd();
|
|
const errors = [];
|
|
const warnings = [];
|
|
|
|
const namePattern = /^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/;
|
|
const banned = [/git\.maks-it\.com/i, /C:\\Users\\maksym/i, /C:\/Users\/maksym/i];
|
|
|
|
function addError(message) {
|
|
errors.push(message);
|
|
}
|
|
|
|
function addWarning(message) {
|
|
warnings.push(message);
|
|
}
|
|
|
|
async function pathExists(target) {
|
|
try {
|
|
await fs.access(target);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function readJson(filePath, context) {
|
|
let raw;
|
|
try {
|
|
raw = await fs.readFile(filePath, "utf8");
|
|
} catch {
|
|
addError(`${context} is missing: ${filePath}`);
|
|
return null;
|
|
}
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch (error) {
|
|
addError(`${context} contains invalid JSON (${filePath}): ${error.message}`);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function parseFrontmatter(content) {
|
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
if (!normalized.startsWith("---\n")) return null;
|
|
const closingIndex = normalized.indexOf("\n---\n", 4);
|
|
if (closingIndex === -1) return null;
|
|
const fields = {};
|
|
for (const line of normalized.slice(4, closingIndex).split("\n")) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
const separator = line.indexOf(":");
|
|
if (separator === -1) continue;
|
|
fields[line.slice(0, separator).trim()] = line.slice(separator + 1).trim();
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
async function walk(dir, acc = []) {
|
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
|
|
const full = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) await walk(full, acc);
|
|
else acc.push(full);
|
|
}
|
|
return acc;
|
|
}
|
|
|
|
async function main() {
|
|
const pluginPath = path.join(root, ".cursor-plugin", "plugin.json");
|
|
const plugin = await readJson(pluginPath, "Plugin manifest");
|
|
if (plugin) {
|
|
if (typeof plugin.name !== "string" || !namePattern.test(plugin.name)) {
|
|
addError(`plugin.json name must be lowercase kebab-case, got ${JSON.stringify(plugin.name)}`);
|
|
}
|
|
if (!plugin.displayName) addWarning("plugin.json is missing displayName");
|
|
if (!plugin.description) addError("plugin.json is missing description");
|
|
if (!plugin.license) addError("plugin.json is missing license");
|
|
if (!plugin.author?.name) addError("plugin.json is missing author.name");
|
|
if (!plugin.author?.email) addError("plugin.json is missing author.email");
|
|
if (!plugin.homepage) addError("plugin.json is missing homepage");
|
|
if (!plugin.repository) addError("plugin.json is missing repository");
|
|
if (plugin.logo) {
|
|
const logoPath = path.join(root, plugin.logo);
|
|
if (!(await pathExists(logoPath))) addError(`logo not found: ${plugin.logo}`);
|
|
}
|
|
const vars = plugin.variables?.properties || {};
|
|
for (const required of plugin.variables?.required || []) {
|
|
if (!vars[required]) addError(`variables.required lists ${required} but it is not in properties`);
|
|
}
|
|
}
|
|
|
|
const mcp = await readJson(path.join(root, "mcp.json"), "mcp.json");
|
|
if (mcp?.mcpServers?.gitea) {
|
|
const env = mcp.mcpServers.gitea.env || {};
|
|
if (env.GITEA_URL !== "${GITEA_URL}") addError("mcp.json must use ${GITEA_URL} placeholder");
|
|
if (env.GITEA_TOKEN !== "${GITEA_TOKEN}") addError("mcp.json must use ${GITEA_TOKEN} placeholder");
|
|
const args = mcp.mcpServers.gitea.args || [];
|
|
if (!args.some((arg) => String(arg).includes("server/dist/index.js"))) {
|
|
addError("mcp.json should launch ${PLUGIN_ROOT}/server/dist/index.js");
|
|
}
|
|
} else {
|
|
addError("mcp.json must define mcpServers.gitea");
|
|
}
|
|
|
|
if (!(await pathExists(path.join(root, "server", "dist", "index.js")))) {
|
|
addError("server/dist/index.js is missing; run npm run build before publishing");
|
|
}
|
|
if (!(await pathExists(path.join(root, "LICENSE")))) addError("LICENSE is missing");
|
|
if (!(await pathExists(path.join(root, "README.md")))) addError("README.md is missing");
|
|
if (!(await pathExists(path.join(root, "SECURITY.md")))) addError("SECURITY.md is missing");
|
|
|
|
const skillDirs = await fs.readdir(path.join(root, "skills")).catch(() => []);
|
|
for (const name of skillDirs) {
|
|
const skillFile = path.join(root, "skills", name, "SKILL.md");
|
|
if (!(await pathExists(skillFile))) {
|
|
addError(`skill ${name} is missing SKILL.md`);
|
|
continue;
|
|
}
|
|
const fields = parseFrontmatter(await fs.readFile(skillFile, "utf8"));
|
|
if (!fields?.name || !fields?.description) {
|
|
addError(`skills/${name}/SKILL.md needs name and description frontmatter`);
|
|
}
|
|
}
|
|
|
|
for (const folder of ["commands", "agents"]) {
|
|
const dir = path.join(root, folder);
|
|
if (!(await pathExists(dir))) continue;
|
|
const files = (await fs.readdir(dir)).filter((name) => name.endsWith(".md"));
|
|
for (const file of files) {
|
|
const fields = parseFrontmatter(await fs.readFile(path.join(dir, file), "utf8"));
|
|
if (!fields?.name || !fields?.description) {
|
|
addError(`${folder}/${file} needs name and description frontmatter`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const ruleFile = path.join(root, "rules", "gitea-workflow.mdc");
|
|
if (await pathExists(ruleFile)) {
|
|
const fields = parseFrontmatter(await fs.readFile(ruleFile, "utf8"));
|
|
if (!fields?.description) addError("rules/gitea-workflow.mdc needs description frontmatter");
|
|
}
|
|
|
|
const files = await walk(root);
|
|
for (const file of files) {
|
|
if (!/\.(md|mdc|json|ts|mjs|example)$/i.test(file)) continue;
|
|
if (file.endsWith(`validate-plugin.mjs`)) continue;
|
|
const text = await fs.readFile(file, "utf8");
|
|
for (const pattern of banned) {
|
|
if (pattern.test(text)) {
|
|
addError(`banned personal host/path in ${path.relative(root, file)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const warning of warnings) console.warn(`warning: ${warning}`);
|
|
for (const error of errors) console.error(`error: ${error}`);
|
|
|
|
if (errors.length) {
|
|
console.error(`\nvalidate-plugin failed with ${errors.length} error(s).`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`validate-plugin passed with ${warnings.length} warning(s).`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|