import { spawnSync } from "child_process" import { readFileSync, writeFileSync } from "fs" import { tool } from "@opencode-ai/plugin" type CustomSection = { title: string; content: string } type CreateArgs = { repo_name: string tagline: string problem_en: string solution_en: string problem_ru: string solution_ru: string quick_start: string tier?: "flagship" | "utility" telegram?: string demo_gif?: string custom_sections_en?: CustomSection[] custom_sections_ru?: CustomSection[] } function extractBetween(text: string, start: string, end: string): string | null { const s = text.indexOf(start) const e = text.indexOf(end) if (s === -1 || e === -1 || e <= s) return null return text.substring(s + start.length, e) } function generateReadme(args: CreateArgs): string { const customEn = (args.custom_sections_en || []) .map((s) => `\n\n### ${s.title}\n${s.content}`) .join("") const customRu = (args.custom_sections_ru || []) .map((s) => `\n\n### ${s.title}\n${s.content}`) .join("") const demoLine = args.tier === "flagship" && args.demo_gif ? `\n![Demo](${args.demo_gif})\n` : "" const telegramLine = args.telegram ? `💬 **Direct Telegram:** [@${args.telegram}](https://t.me/${args.telegram})` : "" return `# 🚀 ${args.repo_name} > ${args.tagline} [English](#-english) | [Русский](#-русская-версия) --- ## 🇬🇧 English ### 🔴 Problem ${args.problem_en} ### 🟢 Solution ${args.solution_en}${customEn} ${demoLine} ### ⚡ Quick Start \`\`\`bash git clone https://github.com/slaid098/${args.repo_name}.git ${args.quick_start} \`\`\` --- ## 🇷🇺 Русская версия ### 🔴 Проблема ${args.problem_ru} ### 🟢 Решение ${args.solution_ru}${customRu} ### ⚡ Быстрый старт \`\`\`bash git clone https://github.com/slaid098/${args.repo_name}.git ${args.quick_start} \`\`\` --- ## 💬 Support & Contact / Поддержка и связь Have questions, need custom features, or want to support this project? 👉 **[Visit Support & Contact Page](https://slaid098.dev/support)** ${telegramLine} ` } function validateReadme(content: string): { ok: boolean; issues: string[] } { const issues: string[] = [] if (!content.includes("")) issues.push("Missing delimiter") if (!content.includes("")) issues.push("Missing delimiter") if (!content.includes("")) issues.push("Missing delimiter") if (!content.includes("")) issues.push("Missing delimiter") const enContent = extractBetween( content, "", "", ) if (enContent !== null && !enContent.trim()) issues.push("EN summary content between delimiters is empty") const ruContent = extractBetween( content, "", "", ) if (ruContent !== null && !ruContent.trim()) issues.push("RU summary content between delimiters is empty") if (!content.includes("slaid098.dev/support")) issues.push("Missing Support & Contact link (slaid098.dev/support)") if (!content.includes("Quick Start")) issues.push("Missing 'Quick Start' section (English)") if (!content.includes("Быстрый старт")) issues.push("Missing 'Быстрый старт' section (Russian)") if (!content.includes("[English]")) issues.push("Missing [English] language switcher link") if (!content.includes("[Русский]")) issues.push("Missing [Русский] language switcher link") return { ok: issues.length === 0, issues } } export default tool({ description: "Create or validate README.md for slaid098 repositories. 'create' mode generates a standardized bilingual README with delimiter tags (, ) parsed by the slaid098.dev showcase. 'validate' mode checks an existing README against the standard. Supports local file (fs) and remote (gh api repos/{owner}/{repo}/contents/README.md) operation.", args: { mode: tool.schema .enum(["create", "validate"]) .describe("Operation mode: 'create' generates README, 'validate' checks existing README structure"), repo_name: tool.schema .string() .optional() .describe("Repository name (e.g. 'anti-detect-mcp'). Required for create mode."), tagline: tool.schema .string() .optional() .describe("Short English tagline (1 sentence). Required for create mode."), problem_en: tool.schema .string() .optional() .describe("Problem statement in English (1-2 sentences). Required for create mode."), solution_en: tool.schema .string() .optional() .describe("Solution in English (1-2 sentences). Required for create mode."), problem_ru: tool.schema .string() .optional() .describe("Problem statement in Russian (1-2 sentences). Required for create mode."), solution_ru: tool.schema .string() .optional() .describe("Solution in Russian (1-2 sentences). Required for create mode."), quick_start: tool.schema .string() .optional() .describe("Install/setup command (e.g. 'pip install -r requirements.txt'). Required for create mode."), tier: tool.schema .enum(["flagship", "utility"]) .optional() .describe("Repository tier: 'flagship' (enables demo_gif) or 'utility' (minimal). Default: 'utility'."), telegram: tool.schema .string() .optional() .describe("Telegram username without @. Optional; adds Direct Telegram line to Support section."), demo_gif: tool.schema .string() .optional() .describe("Path or URL to demo GIF. Only emitted when tier='flagship'."), custom_sections_en: tool.schema .array( tool.schema.object({ title: tool.schema.string(), content: tool.schema.string(), }), ) .optional() .describe("Additional sections rendered inside EN delimiters (after Solution)."), custom_sections_ru: tool.schema .array( tool.schema.object({ title: tool.schema.string(), content: tool.schema.string(), }), ) .optional() .describe("Additional sections rendered inside RU delimiters (after Решение)."), repo: tool.schema .string() .optional() .describe("owner/name for remote operation via GitHub API. If omitted, operates locally on file_path."), file_path: tool.schema .string() .optional() .describe("Local file path (local mode only). Default: 'README.md'."), }, async execute(args, context) { try { const file_path = args.file_path ?? "README.md" const tier = args.tier ?? "utility" if (args.mode === "create") { const required: Record = { repo_name: args.repo_name, tagline: args.tagline, problem_en: args.problem_en, solution_en: args.solution_en, problem_ru: args.problem_ru, solution_ru: args.solution_ru, quick_start: args.quick_start, } for (const [k, v] of Object.entries(required)) { if (!v) return `❌ ${k} is required for create mode` } const content = generateReadme({ repo_name: args.repo_name!, tagline: args.tagline!, problem_en: args.problem_en!, solution_en: args.solution_en!, problem_ru: args.problem_ru!, solution_ru: args.solution_ru!, quick_start: args.quick_start!, tier, telegram: args.telegram, demo_gif: args.demo_gif, custom_sections_en: args.custom_sections_en, custom_sections_ru: args.custom_sections_ru, }) if (args.repo) { const getRes = spawnSync( "gh", ["api", `repos/${args.repo}/contents/README.md`], { encoding: "utf-8", cwd: context.worktree }, ) let sha: string | undefined if (getRes.status === 0) { try { sha = JSON.parse(getRes.stdout).sha } catch { sha = undefined } } const content64 = Buffer.from(content, "utf-8").toString("base64") const putArgs = [ "api", "-X", "PUT", `repos/${args.repo}/contents/README.md`, "-f", "message=docs: update README", "-f", `content=${content64}`, ] if (sha) putArgs.push("-f", `sha=${sha}`) const putRes = spawnSync("gh", putArgs, { encoding: "utf-8", cwd: context.worktree, }) if (putRes.status !== 0) { return `⚠️ create-readme failed: gh api PUT failed (exit ${putRes.status}): ${putRes.stderr || putRes.stdout}` } return `README.md updated in ${args.repo} via GitHub API` } writeFileSync(file_path, content, "utf-8") return `README.md created at ${file_path}` } let content: string if (args.repo) { const getRes = spawnSync( "gh", ["api", `repos/${args.repo}/contents/README.md`], { encoding: "utf-8", cwd: context.worktree }, ) if (getRes.status !== 0) { return `⚠️ create-readme failed: gh api GET failed (exit ${getRes.status}): ${getRes.stderr || getRes.stdout}` } const data = JSON.parse(getRes.stdout) content = Buffer.from(data.content, "base64").toString("utf-8") } else { content = readFileSync(file_path, "utf-8") } const result = validateReadme(content) if (result.ok) return "✅ README structure is valid" return `❌ Validation issues:\n${result.issues.map((i) => `- ${i}`).join("\n")}` } catch (e) { return `⚠️ create-readme failed: ${e instanceof Error ? e.message : String(e)}` } }, })