diff --git a/.opencode/skills/repo-readme/SKILL.md b/.opencode/skills/repo-readme/SKILL.md new file mode 100644 index 0000000..cd730a2 --- /dev/null +++ b/.opencode/skills/repo-readme/SKILL.md @@ -0,0 +1,162 @@ +--- +name: repo-readme +description: Use when creating or updating README.md for any slaid098 repository. Calls create-readme tool for deterministic structure with delimiter tags for slaid098.dev showcase. Also when user says "оформи README", "обнови описание репо", "readme template", "базовая структура readme". +--- + +# Repo README + +Стандартизация `README.md` во всех репозиториях slaid098 через тулзу +`create-readme`. Тулза гарантирует структуру (разделители, Support block, +Quick Start, language switcher). Скилл даёт контекст: когда вызывать тулзу и +какие параметры передавать. + +## 1. Когда использовать тулзу + +- **Новый репо** → `create-readme` (mode: `create`) — генерирует + стандартизированный README с нуля. +- **Проверка существующего README** → `create-readme` (mode: `validate`) — + проверяет, что структура соответствует стандарту витрины. +- **После ручных правок README** → всегда `validate`. Любая правка руками + агента (через Edit/Write) может нарушить разделители — после правок + обязательна валидация. + +Не генерируй README вручную через Write — структура критична для парсинга +витриной. Только через тулзу `create-readme`. + +## 2. Tier system + +Тулза принимает параметр `tier`: + +- **`flagship`** — флагманские проекты (Anti-Detect MCP, Mobile Whisper и + т.п.). Полный набор параметров: `demo_gif` (демо-гифка), `custom_sections`, + `telegram`. Используется для заметных проектов на витрине. +- **`utility`** (default) — бытовые утилиты. Минимальный набор: `problem` / + `solution` (EN+RU), `quick_start`. `demo_gif` игнорируется. + +Выбор tier влияет только на блок `demo_gif` после EN-разделителей. Остальная +структура одинакова для обоих tier'ов. + +## 3. GitHub metadata (отдельный шаг, НЕ в тулзе) + +`create-readme` отвечает только за файл `README.md`. Метаданные репозитория +настраиваются отдельно через `gh repo edit` (это bash, не тулза): + +- Описание: `gh repo edit --description "короткое описание"` +- Topics для поиска: `gh repo edit --add-topic topic1 --add-topic topic2` +- Social preview image — через настройки GitHub UI (не CLI). + +Метаданные не дублируют README — они для карточки репо на GitHub и поиска. + +## 4. Workflow + +1. `create-readme` (mode: `create`) → генерирует README с гарантированной + структурой. +2. Ручные правки если нужно (агент редактирует файл напрямую через Edit) — + например, расширить `custom_sections`, поправить формулировки. +3. `create-readme` (mode: `validate`) → проверяет, что структура не нарушена. +4. Если `validate` fails → фикс нарушения → re-`validate`. Цикл пока не + пройдёт. + +Локальный режим (по умолчанию): тулза пишет в `file_path` (default +`README.md`) через `fs.writeFileSync`. Удалённый режим: передай `repo` +(`owner/name`) — тулза сделает PUT через `gh api +repos/{owner}/{repo}/contents/README.md` с base64-контентом и SHA. + +## 5. Зачем разделители (контекст для агента) + +Витрина **slaid098.dev** скачивает raw `README.md` из каждого репо и +извлекает фрагмент между разделителями: + +- `` ... `` — английский + блок для карточки. +- `` ... `` — русский блок. + +Поэтому структура README **должна быть гарантирована тулзой**, а не агентом. +Агент может менять текст *внутри* разделителей, но не должен удалять/двигать +сами разделители. `validate` ловит такие нарушения. + +## 6. Шаблон README (reference) + +Тулза `create-readme` генерирует ровно эту структуру (параметры подставляются): + +```markdown +# 🚀 {repo_name} +> {tagline} + +[English](#-english) | [Русский](#-русская-версия) + +--- + +## 🇬🇧 English + + +### 🔴 Problem +{problem_en} + +### 🟢 Solution +{solution_en} +{custom_sections_en — доп. секции, если переданы} + +{demo_gif строка, только если tier=flagship} + +### ⚡ Quick Start +\`\`\`bash +git clone https://github.com/slaid098/{repo_name}.git +{quick_start} +\`\`\` + +--- + +## 🇷🇺 Русская версия + + +### 🔴 Проблема +{problem_ru} + +### 🟢 Решение +{solution_ru} +{custom_sections_ru — доп. секции, если переданы} + + +### ⚡ Быстрый старт +\`\`\`bash +git clone https://github.com/slaid098/{repo_name}.git +{quick_start} +\`\`\` + +--- + +## 💬 Support & Contact / Поддержка и связь + +Have questions, need custom features, or want to support this project? +👉 **[Visit Support & Contact Page](https://slaid098.dev/support)** +{telegram строка, если передан} +``` + +`validate` проверяет: наличие обоих EN/RU разделителей, непустой контент +между ними, ссылку `slaid098.dev/support`, секции Quick Start (EN) и Быстрый +старт (RU), language switcher `[English]` / `[Русский]`. + +## 7. Независимость от repo-init + +- Скилл `repo-init` создаёт **пустой** `README.md` как часть инициализации + репо. +- `repo-readme` (через тулзу `create-readme`) **наполняет** его + стандартизированным контентом. +- Может применяться к существующим репо без `repo-init` — тулза перезапишет + `README.md` (локально) или обновит через GitHub API (с SHA). + +## 8. Параметры тулзы (кратко) + +`create-readme`: + +- `mode` — `"create"` | `"validate"` (обязательный). +- `repo_name`, `tagline`, `problem_en`, `solution_en`, `problem_ru`, + `solution_ru`, `quick_start` — обязательны для `create`. +- `tier` — `"flagship"` | `"utility"` (default `utility`). +- `telegram` — username без `@` (optional). +- `demo_gif` — путь/URL (только для `flagship`). +- `custom_sections_en` / `custom_sections_ru` — массивы + `{ title, content }` (optional). +- `repo` — `owner/name` для удалённой операции (optional). +- `file_path` — локальный путь (default `README.md`). diff --git a/.opencode/tools/create-readme.ts b/.opencode/tools/create-readme.ts new file mode 100644 index 0000000..a420f3f --- /dev/null +++ b/.opencode/tools/create-readme.ts @@ -0,0 +1,305 @@ +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)}` + } + }, +}) diff --git a/docs/decisions/049-pr-112-add-repo-readme-skill-and-tool.md b/docs/decisions/049-pr-112-add-repo-readme-skill-and-tool.md new file mode 100644 index 0000000..e65ac8c --- /dev/null +++ b/docs/decisions/049-pr-112-add-repo-readme-skill-and-tool.md @@ -0,0 +1,60 @@ +# ADR-049: create-readme tool + repo-readme skill for standardized README + +## Статус +Accepted (2026-07-28) + +## Контекст + +Витрина slaid098.dev парсит `README.md` из каждого репозитория: скачивает raw +файл и извлекает фрагмент между разделителями `` и +``. До сих пор README формировался агентом вручную +через Write/Edit — это недетерминированно: разделители могли отсутствовать, +секции Support/Quick Start/language switcher — расходиться со стандартом, из-за +чего витрина не могла корректно отрендерить карточку. Issue #111 требует тулзу, +гарантирующую структуру, и скилл, дающий агенту контекст. + +## Решение + +Реализовать TS-плагин `create-readme` (`.opencode/tools/create-readme.ts`) с +двумя режимами: +- `create` — генерирует README по фиксированному шаблону через template + literal, подставляя параметры. Структура (разделители, Support block, Quick + Start, language switcher) зашита в тулзе, а не в агенте. +- `validate` — проверяет существующий README на соответствие стандарту (6 + проверок). + +Параметр `mode` = `tool.schema.enum(["create","validate"])` (zod через +`tool.schema` = `typeof z`, подтверждено `tool.d.ts`). `tier` (flagship/utility) +и `file_path` реализованы через `.optional()` + fallback в handler (`args.tier +?? "utility"`) вместо zod `.default()` — консистентно с существующими тулзами +(они `.default()` не используют) и надёжно независимо от того, парсит opencode +args через zod или передаёт raw. + +Локальный режим: `fs.writeFileSync`/`readFileSync`. Удалённый режим: `gh api` +через `spawnSync` (GET для SHA, PUT с base64+content+message+sha) — как +`create-issue.ts`/`commit.ts` используют `spawnSync` напрямую. `gh api -X PUT` +помечен как destructive в `check-permissions.py`, но это правило проверяет +bash allow-rules конфига, а не `spawnSync` внутри плагина — тулзе allow-rule не +нужен. + +Скилл `repo-readme` описывает: когда вызывать create vs validate, tier system, +GitHub metadata как отдельный шаг (`gh repo edit`, не в тулзе), workflow +(create → ручные правки → validate), зачем разделители, шаблон как reference, +независимость от repo-init. + +## Альтернативы + +- **zod `discriminatedUnion("mode", [createSchema, validateSchema])`** — + отвергнуто: `tool()` принимает `args: Args extends z.ZodRawShape` (plain + object of zod schemas), а не `ZodDiscriminatedUnion`. Использована плоская + schema с `mode: enum` + ручная валидация обязательных полей в handler. +- **zod `.default()` для tier/file_path** — отвергнуто ради консистентности с + существующими тулзами и независимости от того, вызывает opencode zod-parse. + Использован `.optional()` + fallback `??`. +- **Импорт zod напрямую** — отвергнут: `tool.schema` уже re-export'ит полный + zod (`var schema: typeof z`), отдельный импорт и зависимость `zod` в + package.json не нужны (его нет в dependencies, и существующие тулзы его не + импортируют). +- **Регистрация тулзы в `opencode.json` (agent.tools)** — не требуется: + плагины авто-дискаверятся из `.opencode/tools/*.ts` (как `tunnel.ts`, + которого нет в конфиге). diff --git a/docs/handoff/pr-112-add-repo-readme-skill-and-tool.md b/docs/handoff/pr-112-add-repo-readme-skill-and-tool.md new file mode 100644 index 0000000..578eeea --- /dev/null +++ b/docs/handoff/pr-112-add-repo-readme-skill-and-tool.md @@ -0,0 +1,49 @@ +--- +pr: 112 +title: feat(skills): add repo-readme skill and create-readme tool +--- + +## Что сделано + +Создана тулза `create-readme` (`.opencode/tools/create-readme.ts`, TS-плагин) с +двумя режимами: +- `create` — генерирует стандартизированный двуязычный README.md по шаблону с + разделителями `` и ``, + Support & Contact блоком, Quick Start, language switcher. Поддерживает + `custom_sections_en/ru`, `tier` (flagship/utility), `demo_gif`, `telegram`. + Локальный режим (`fs.writeFileSync` в `file_path`) и удалённый (`gh api + repos/{owner}/{repo}/contents/README.md` PUT с base64 + SHA). +- `validate` — проверяет существующий README на 6 критериев: наличие обоих + EN/RU разделителей, непустой контент между ними, ссылку slaid098.dev/support, + секции Quick Start (EN) + Быстрый старт (RU), language switcher. + +Создан скилл `repo-readme` (`.opencode/skills/repo-readme/SKILL.md`): когда +использовать create vs validate, tier system, GitHub metadata как отдельный +шаг, workflow (create → ручные правки → validate), зачем разделители (парсинг +витриной slaid098.dev), шаблон README как reference, независимость от +repo-init, краткий список параметров тулзы. + +Проверки: `tsc --noEmit` чисто; `check-permissions.py` OK (тулза вызывает +`gh api` через `spawnSync` внутри плагина — не bash-команда агента, не требует +allow-rule). + +## Почему + +Структура README критична для витрины slaid098.dev — она скачивает raw +README и извлекает фрагмент между разделителями. Тулза обеспечивает +детерминированную генерацию структуры, скилл даёт контекст агенту (когда +вызывать, tier, metadata). Ручная генерация README агентом через Write +рискует нарушить разделители. + +## Pending + +— + +## Watch out + +Handoff/ADR файлы названы `pr-0-*` (placeholder) — номер PR подставится после +create-pr и отдельного коммита `docs(handoff): set PR number`. Тулза +регистрации в `opencode.json` не требует (плагины авто-дискаверятся из +`.opencode/tools/*.ts`, как `tunnel.ts`). `tier`/`file_path` реализованы через +`.optional()` + fallback в handler (не `.default()`), консистентно с +существующими тулзами (commit.ts, create-issue.ts не используют zod `.default()`). diff --git a/docs/project-map/README.md b/docs/project-map/README.md index bdbdcba..537d05a 100644 --- a/docs/project-map/README.md +++ b/docs/project-map/README.md @@ -35,6 +35,7 @@ opencode-config/ │ │ ├── python-development/SKILL.md # Python dev patterns │ │ ├── release/SKILL.md # Tag + GitHub Release │ │ ├── repo-init/SKILL.md # New repository bootstrap +│ │ ├── repo-readme/SKILL.md # Standardized README generation (create-readme tool: create vs validate, tier system, GitHub metadata) — PR#112 │ │ ├── tunnel/SKILL.md # Cloudflare tunnel toggle (tool `tunnel()`: 1-й вызов start, 2-й stop) — PR#63 (восстановлен, удалён в PR#42) │ │ ├── run-tests/SKILL.md # Test runner guide │ │ └── spec/SKILL.md # 9-phase spec generation @@ -43,6 +44,7 @@ opencode-config/ │ │ ├── commit.ts # commit tool wrapper (1 arg message, validates format+staged) — PR#38 │ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates format+labels; optional repo?: string) — PR#38, PR#65 │ │ ├── create-pr.ts # create-pr tool wrapper (3 args, validates format+Closes #N; optional repo?: string) — PR#38, PR#65 +│ │ ├── create-readme.ts # create-readme tool (TS plugin, modes: create/validate; standardized bilingual README with slaid098.dev delimiters; local fs + remote gh api) — PR#112 │ │ ├── merge-pr.ts # merge-pr tool wrapper (orchestrator-safe gh pr merge; optional repo?: string) — PR#30, PR#65 │ │ ├── memory-access.ts # memory-access tool (bump frontmatter last_accessed/access_count, regex replace, atomic write tmp+rename) — PR#101 │ │ ├── memory-doctor.ts # memory-doctor tool (read-only diagnostics: rg, Python src.memory importability, env vars, memory dir, RAG index; markdown ✅/❌ report) — PR#101