import { spawnSync } from "child_process" import { writeFileSync } from "fs" import path from "path" import { tool } from "@opencode-ai/plugin" type Feature = { emoji: string; name: string; description: string } type CustomSection = { title: string; content: string } type CreateArgs = { repo_name: string tagline_en: string tagline_ru: string why_en: string what_en: string why_ru: string what_ru: string quick_start?: string features_en: Feature[] features_ru: Feature[] access_url?: string include_clone?: boolean development_en?: string development_ru?: string custom_sections_en?: CustomSection[] custom_sections_ru?: CustomSection[] quick_start_steps_en?: string[] quick_start_steps_ru?: string[] } function renderFeaturesTable(features: Feature[], isRu: boolean): string { const header = isRu ? "### Фичи\n\n| Фича | Описание |\n|------|----------|\n" : "### Features\n\n| Feature | Description |\n|---------|-------------|\n" const rows = features .map((f) => `| ${f.emoji} ${f.name} | ${f.description} |`) .join("\n") return header + rows } function renderSteps(steps?: string[]): string { if (!steps || steps.length === 0) return "" const items = steps.map((s, i) => `${i + 1}. ${s}`).join("\n") return `\n${items}\n` } 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 accessLineEn = args.access_url ? `\nAccess at [${args.access_url}](${args.access_url})\n` : "" const accessLineRu = args.access_url ? `\nДоступ: [${args.access_url}](${args.access_url})\n` : "" const developmentBlockEn = args.development_en ? `\n\n### 🔧 Development\n${args.development_en}\n` : "" const developmentBlockRu = args.development_ru ? `\n\n### 🔧 Разработка\n${args.development_ru}\n` : "" const cloneLine = args.include_clone !== false ? `git clone https://github.com/slaid098/${args.repo_name}.git\n` : "" const hasBashBlock = args.include_clone !== false || (args.quick_start ?? "") !== "" const qs = args.quick_start ?? "" const bashBlockEn = hasBashBlock ? `\`\`\`bash\n${cloneLine}${qs}\n\`\`\`` : "" const bashBlockRu = hasBashBlock ? `\`\`\`bash\n${cloneLine}${qs}\n\`\`\`` : "" const stepsEn = renderSteps(args.quick_start_steps_en) const stepsRu = renderSteps(args.quick_start_steps_ru) return `# 🚀 ${args.repo_name} ![Cover](assets/cover.png) > ${args.tagline_en} > ${args.tagline_ru} [English](#-english) | [Русский](#-русский) --- ## 🇺🇸 English ### ❓ Why ${args.why_en} ### ✅ What ${args.what_en} ${renderFeaturesTable(args.features_en, false)} ${customEn} ### ⚡ Quick Start ${bashBlockEn}${stepsEn}${accessLineEn}${developmentBlockEn} --- ## 🇷🇺 Русский ### ❓ Зачем ${args.why_ru} ### ✅ Что ${args.what_ru} ${renderFeaturesTable(args.features_ru, true)} ${customRu} ### ⚡ Быстрый старт ${bashBlockRu}${stepsRu}${accessLineRu}${developmentBlockRu} --- ## 💬 Support and contacts / Поддержка и контакты 👉 **[slaid098.dev/contacts](https://slaid098.dev/contacts)** ` } export default tool({ description: "Create README.md for slaid098 repositories. Generates a standardized bilingual README with delimiter tags (, , , ) parsed by the slaid098.dev showcase. Includes the '## 🇷🇺 Русский' header and the [Русский](#-русский) switcher anchor. Supports local file (fs) and remote (gh api repos/{owner}/{repo}/contents/README.md) operation. README validation is handled by .opencode/scripts/project-status.py:check_readme (read-only architecture oracle).", args: { repo_name: tool.schema .string() .optional() .describe("Repository name (e.g. 'anti-detect-mcp'). Required."), tagline_en: tool.schema .string() .optional() .describe("Short English tagline (1 sentence). Required for create mode. Must not contain Cyrillic."), tagline_ru: tool.schema .string() .optional() .describe("Short Russian tagline (1 sentence). Required for create mode. Must contain Cyrillic."), why_en: tool.schema .string() .optional() .describe("Why this exists — in English (1-2 sentences). Required for create mode."), what_en: tool.schema .string() .optional() .describe("What it does — in English (1-2 sentences). Required for create mode."), why_ru: tool.schema .string() .optional() .describe("Зачем этот проект — на русском (1-2 предложения). Required for create mode."), what_ru: tool.schema .string() .optional() .describe("Что делает — на русском (1-2 предложения). Required for create mode."), quick_start: tool.schema .string() .optional() .describe("Install/setup command (e.g. 'pip install -r requirements.txt'). Optional — omit or pass '' when setup is steps-only (use quick_start_steps_*); bash block omitted when empty AND include_clone is false."), quick_start_steps_en: tool.schema .array(tool.schema.string()) .optional() .describe("Array of raw-markdown strings rendered as a numbered list of clickable steps after the EN Quick Start bash block. Each string = one step, may contain markdown links [text](url). Omit to render only the bash block."), quick_start_steps_ru: tool.schema .array(tool.schema.string()) .optional() .describe("Array of raw-markdown strings rendered as a numbered list of clickable steps after the RU Быстрый старт bash block. Each string = one step, may contain markdown links [text](url). Omit to render only the bash block."), features_en: tool.schema .array( tool.schema.object({ emoji: tool.schema.string(), name: tool.schema.string(), description: tool.schema.string(), }), ) .optional() .describe("Array of features for EN table. Each: { emoji, name, description }. Required for create mode."), features_ru: tool.schema .array( tool.schema.object({ emoji: tool.schema.string(), name: tool.schema.string(), description: tool.schema.string(), }), ) .optional() .describe("Array of features for RU table. Each: { emoji, name, description }. Required for create mode."), custom_sections_en: tool.schema .array( tool.schema.object({ title: tool.schema.string(), content: tool.schema.string(), }), ) .optional() .describe("Additional sections rendered after EN features block (outside delimiters)."), custom_sections_ru: tool.schema .array( tool.schema.object({ title: tool.schema.string(), content: tool.schema.string(), }), ) .optional() .describe("Additional sections rendered after RU features block (outside delimiters)."), 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'."), access_url: tool.schema .string() .optional() .describe("Optional URL for Access/Доступ line after Quick Start bash block. EN: 'Access at {url}', RU: 'Доступ: {url}'. Omit if no web access."), include_clone: tool.schema .boolean() .optional() .describe("If false, omits 'git clone' from Quick Start bash block. Default true (backward compatible). Set false for userscripts, web apps, npm packages."), development_en: tool.schema .string() .optional() .describe("Raw markdown for ### 🔧 Development section after EN Quick Start (outside delimiters, not shown on slaid098.dev). Omit if no dev section needed."), development_ru: tool.schema .string() .optional() .describe("Raw markdown for ### 🔧 Разработка section after RU Быстрый старт (outside delimiters, not shown on slaid098.dev). Omit if no dev section needed."), }, async execute(args, context) { try { const file_path = args.file_path ?? "README.md" const absPath = path.resolve(context.worktree, file_path) const required: Record = { repo_name: args.repo_name, tagline_en: args.tagline_en, tagline_ru: args.tagline_ru, why_en: args.why_en, what_en: args.what_en, why_ru: args.why_ru, what_ru: args.what_ru, } for (const [k, v] of Object.entries(required)) { if (!v) return `❌ ${k} is required for create mode` } if (!args.features_en || args.features_en.length === 0) return `❌ features_en is required for create mode` if (!args.features_ru || args.features_ru.length === 0) return `❌ features_ru is required for create mode` if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(args.repo_name!)) return `❌ repo_name must be lowercase kebab-case (a-z0-9 with single dashes), got: "${args.repo_name}"` if (/[ЁА-яё]/.test(args.tagline_en!)) return `❌ tagline_en must be English (no Cyrillic), got Cyrillic chars` if (!/[ЁА-яё]/.test(args.tagline_ru!)) return `❌ tagline_ru must contain Cyrillic (Russian text), got: "${args.tagline_ru}"` const hasBash = args.include_clone !== false || (args.quick_start ?? "") !== "" const hasStepsEn = !!args.quick_start_steps_en && args.quick_start_steps_en.length > 0 const hasStepsRu = !!args.quick_start_steps_ru && args.quick_start_steps_ru.length > 0 if (!hasBash && !hasStepsEn) return `❌ quick_start or quick_start_steps_en required (Quick Start EN would be empty)` if (!hasBash && !hasStepsRu) return `❌ quick_start or quick_start_steps_ru required (Quick Start RU would be empty)` const content = generateReadme({ repo_name: args.repo_name!, tagline_en: args.tagline_en!, tagline_ru: args.tagline_ru!, why_en: args.why_en!, what_en: args.what_en!, why_ru: args.why_ru!, what_ru: args.what_ru!, quick_start: args.quick_start, features_en: args.features_en!, features_ru: args.features_ru!, access_url: args.access_url, include_clone: args.include_clone, development_en: args.development_en, development_ru: args.development_ru, custom_sections_en: args.custom_sections_en, custom_sections_ru: args.custom_sections_ru, quick_start_steps_en: args.quick_start_steps_en, quick_start_steps_ru: args.quick_start_steps_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(absPath, content, "utf-8") return `README.md created at ${file_path}` } catch (e) { return `⚠️ create-readme failed: ${e instanceof Error ? e.message : String(e)}` } }, })