From bcfd20bea6714f4edce86100173900b6c742a304 Mon Sep 17 00:00:00 2001 From: Samarth Rayar Date: Fri, 2 Oct 2026 07:39:13 +0000 Subject: [PATCH 1/3] Version check: handle new plugins, count skill Markdown as shipped A plugin with no manifest at the base commit made git show fail under pipefail, so the check exited 128 on any push that added a plugin. Only a plugin's README and CHANGELOG are exempt now: SKILL.md and agent files are Markdown that ships. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QUAMWfzunRcEdkbuZNaq9F --- scripts/check-version-bumps.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/check-version-bumps.sh b/scripts/check-version-bumps.sh index d616a40..48e29d7 100644 --- a/scripts/check-version-bumps.sh +++ b/scripts/check-version-bumps.sh @@ -4,14 +4,16 @@ # # scripts/check-version-bumps.sh [head-ref] # -# Docs, assets and tests don't count as shipped changes. +# A plugin's README, CHANGELOG, assets and tests don't count as shipped +# changes. Skills and agents are Markdown too, so other .md files do. set -euo pipefail base="${1:?usage: check-version-bumps.sh [head-ref]}" head="${2:-HEAD}" +# Empty when the manifest doesn't exist at that commit (a new plugin). version_at() { - git show "$1:$2" 2>/dev/null | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | head -1 + { git show "$1:$2" 2>/dev/null || true; } | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' | head -1 } status=0 @@ -21,7 +23,7 @@ for dir in $(git diff --name-only "$base" "$head" -- plugins | cut -d/ -f1-2 | s [ -n "$(git ls-tree "$head" -- "$manifest")" ] || continue # removed plugin shipped=$(git diff --name-only "$base" "$head" -- "$dir" | - grep -vE '\.md$|/assets/|\.test\.(ts|tsx)$' || true) + grep -vE "^$dir/(README|CHANGELOG)\.md$|/assets/|\.test\.(ts|tsx)$" || true) [ -n "$shipped" ] || continue before=$(version_at "$base" "$manifest") From ecdd47ae6dd85c42a111a4c827fdc7fa0677b698 Mon Sep 17 00:00:00 2001 From: Samarth Rayar Date: Fri, 2 Oct 2026 07:39:13 +0000 Subject: [PATCH 2/3] Generic tooling: catalog check, scaffold script, asset scripts - scripts/catalog.mts checks that plugin folders, marketplace entries, manifests and changelogs agree, and generates the README catalog table. - scripts/new-plugin.mts scaffolds a plugin from templates/ with any of skill, agent, hooks, mcp and mod, and registers it. - Asset generators live in scripts/assets/; CI runs each with --check. - Release notes read the marketplace name instead of hardcoding it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QUAMWfzunRcEdkbuZNaq9F --- .github/workflows/release.yml | 8 +- .github/workflows/validate.yml | 9 +- plugins/buddy/README.md | 2 +- .../buddy-moods.mts} | 12 +- scripts/catalog.mts | 26 ++++ scripts/lib.mts | 137 ++++++++++++++++++ scripts/new-plugin.mts | 113 +++++++++++++++ templates/agent/agent.md | 11 ++ templates/mod/register.test.ts | 6 + templates/mod/register.ts | 12 ++ templates/plugin/CHANGELOG.md | 9 ++ templates/plugin/README.md | 35 +++++ templates/skill/SKILL.md | 9 ++ 13 files changed, 380 insertions(+), 9 deletions(-) rename scripts/{render-buddy-gallery.mts => assets/buddy-moods.mts} (82%) create mode 100644 scripts/catalog.mts create mode 100644 scripts/lib.mts create mode 100644 scripts/new-plugin.mts create mode 100644 templates/agent/agent.md create mode 100644 templates/mod/register.test.ts create mode 100644 templates/mod/register.ts create mode 100644 templates/plugin/CHANGELOG.md create mode 100644 templates/plugin/README.md create mode 100644 templates/skill/SKILL.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5c6c6a..4deb8f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,11 @@ jobs: manifest="plugins/$name/.claude-plugin/plugin.json" changelog="plugins/$name/CHANGELOG.md" + if [ ! -f "$manifest" ]; then + echo "::error::$TAG names no plugin: $manifest does not exist" + exit 1 + fi + actual=$(sed -n 's/.*"version": *"\([^"]*\)".*/\1/p' "$manifest" | head -1) if [ "$actual" != "$version" ]; then echo "::error::$TAG does not match $manifest ($actual)" @@ -40,6 +45,7 @@ jobs: on { print } ' "$changelog" > notes.md [ -s notes.md ] || echo "See $changelog." > notes.md - printf '\nInstall or update:\n\n```bash\nclaude plugin marketplace add %s\nclaude plugin install %s@samarth\n```\n' "$GITHUB_REPOSITORY" "$name" >> notes.md + marketplace=$(jq -r .name .claude-plugin/marketplace.json) + printf '\nInstall or update:\n\n```bash\nclaude plugin marketplace add %s\nclaude plugin install %s@%s\n```\n' "$GITHUB_REPOSITORY" "$name" "$marketplace" >> notes.md gh release create "$TAG" --title "$name $version" --notes-file notes.md --verify-tag diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 277319d..03e9ee1 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -33,8 +33,15 @@ jobs: fi echo "::endgroup::" done + - name: Catalog is consistent + run: node --experimental-strip-types scripts/catalog.mts --check - name: Generated assets are up to date - run: node --experimental-strip-types scripts/render-buddy-gallery.mts --check + run: | + set -e + for script in scripts/assets/*.mts; do + [ -e "$script" ] || continue + node --experimental-strip-types "$script" --check + done - name: Changed plugins bumped their version run: | if [ "${{ github.event_name }}" = "pull_request" ]; then diff --git a/plugins/buddy/README.md b/plugins/buddy/README.md index 6b57587..5196e18 100644 --- a/plugins/buddy/README.md +++ b/plugins/buddy/README.md @@ -78,7 +78,7 @@ From the repository root: claude plugin validate plugins/buddy claude plugin test plugins/buddy claude --plugin-dir plugins/buddy # try it, hot-reloads on save -node --experimental-strip-types scripts/render-buddy-gallery.mts # after changing the sprite +node --experimental-strip-types scripts/assets/buddy-moods.mts # after changing the sprite ``` See [CHANGELOG.md](CHANGELOG.md) for releases. diff --git a/scripts/render-buddy-gallery.mts b/scripts/assets/buddy-moods.mts similarity index 82% rename from scripts/render-buddy-gallery.mts rename to scripts/assets/buddy-moods.mts index 71dff81..bb15db0 100644 --- a/scripts/render-buddy-gallery.mts +++ b/scripts/assets/buddy-moods.mts @@ -1,15 +1,15 @@ // Renders plugins/buddy/assets/moods.svg, the animated mood gallery in buddy's // README, from the same sprite code the mod draws with. // -// node --experimental-strip-types scripts/render-buddy-gallery.mts -// node --experimental-strip-types scripts/render-buddy-gallery.mts --check (CI: fail if stale) +// node --experimental-strip-types scripts/assets/buddy-moods.mts +// node --experimental-strip-types scripts/assets/buddy-moods.mts --check (CI: fail if stale) import { readFileSync, writeFileSync } from 'node:fs' -import { animation, svg } from '../plugins/buddy/hooks/sprite.ts' -import type { Mood } from '../plugins/buddy/types/index.d.ts' +import { animation, svg } from '../../plugins/buddy/hooks/sprite.ts' +import type { Mood } from '../../plugins/buddy/types/index.d.ts' -const OUT = new URL('../plugins/buddy/assets/moods.svg', import.meta.url) +const OUT = new URL('../../plugins/buddy/assets/moods.svg', import.meta.url) const CELLS: { mood: Mood; percent: number; label: string }[] = [ { mood: 'idle', percent: 20, label: 'ready' }, @@ -50,7 +50,7 @@ const gallery = if (process.argv.includes('--check')) { const current = readFileSync(OUT, 'utf8') if (current !== gallery) { - console.error('plugins/buddy/assets/moods.svg is stale: run node --experimental-strip-types scripts/render-buddy-gallery.mts') + console.error('plugins/buddy/assets/moods.svg is stale: run node --experimental-strip-types scripts/assets/buddy-moods.mts') process.exit(1) } console.log('moods.svg is up to date') diff --git a/scripts/catalog.mts b/scripts/catalog.mts new file mode 100644 index 0000000..9e2ad85 --- /dev/null +++ b/scripts/catalog.mts @@ -0,0 +1,26 @@ +// Checks that every plugin folder, marketplace entry, manifest and changelog +// agree, and regenerates the catalog table in the root README from the +// marketplace. +// +// node --experimental-strip-types scripts/catalog.mts write the table +// node --experimental-strip-types scripts/catalog.mts --check CI: fail on drift + +import { MARKETPLACE, catalogErrors, isReadmeCurrent, readJson, updateReadme } from './lib.mts' +import type { Marketplace } from './lib.mts' + +const marketplace = readJson(MARKETPLACE) +const errors = catalogErrors(marketplace) +for (const error of errors) console.error(`::error::${error}`) + +if (process.argv.includes('--check')) { + if (!isReadmeCurrent(marketplace)) { + console.error('::error::The README catalog is stale: run node --experimental-strip-types scripts/catalog.mts') + process.exit(1) + } + if (errors.length > 0) process.exit(1) + console.log(`catalog is consistent: ${marketplace.plugins.length} plugin(s)`) +} else { + updateReadme(marketplace) + console.log('updated the README catalog') + if (errors.length > 0) process.exit(1) +} diff --git a/scripts/lib.mts b/scripts/lib.mts new file mode 100644 index 0000000..c5039f9 --- /dev/null +++ b/scripts/lib.mts @@ -0,0 +1,137 @@ +// Shared helpers for the repo scripts: reading and writing the marketplace, +// and the catalog checks and README table that CI keeps in sync with it. + +import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' + +export const ROOT = new URL('../', import.meta.url) +export const MARKETPLACE = '.claude-plugin/marketplace.json' +const README = 'README.md' +const START = '' +const END = '' + +export type Entry = { + name: string + source: string | { source: string } + description?: string + displayName?: string + category?: string + tags?: string[] + version?: string +} +export type Marketplace = { name: string; owner: { name: string; url?: string }; plugins: Entry[] } +export type Manifest = { name: string; displayName?: string; version?: string; description?: string } + +export const path = (p: string) => new URL(p, ROOT) +export const exists = (p: string) => existsSync(path(p)) +export const read = (p: string) => readFileSync(path(p), 'utf8') +export function write(p: string, text: string): void { + mkdirSync(new URL('./', path(p)), { recursive: true }) + writeFileSync(path(p), text) +} +export const readJson = (p: string): T => JSON.parse(read(p)) +export const writeJson = (p: string, value: unknown) => write(p, formatJson(value) + '\n') + +// JSON.stringify with two-space indents, except that an array or object that +// fits on one line and holds no nested array or object stays on one line, the +// way the manifests in this repo are written by hand. +export function formatJson(value: unknown, indent = ''): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value) + const isArray = Array.isArray(value) + const items = isArray + ? (value as unknown[]).map(v => [undefined, v] as const) + : Object.entries(value as Record) + const [open, close] = isArray ? ['[', ']'] : ['{', '}'] + if (items.length === 0) return open + close + const key = (k: string | undefined) => (k === undefined ? '' : `${JSON.stringify(k)}: `) + const isFlat = items.every(([, v]) => v === null || typeof v !== 'object') + if (isFlat) { + const inner = items.map(([k, v]) => key(k) + JSON.stringify(v)).join(', ') + const line = isArray ? `[${inner}]` : `{ ${inner} }` + if (indent.length + line.length <= 100) return line + } + const next = indent + ' ' + const lines = items.map(([k, v]) => next + key(k) + formatJson(v, next)) + return `${open}\n${lines.join(',\n')}\n${indent}${close}` +} + +export const pluginDirs = () => + readdirSync(path('plugins/'), { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name) + .sort() + +// Every way the catalog can drift: a plugin folder without an entry, an entry +// without a folder, mismatched names, or a release without its changelog. +export function catalogErrors(marketplace: Marketplace): string[] { + const errors: string[] = [] + const dirs = new Set(pluginDirs()) + const listed = new Set() + + for (const entry of marketplace.plugins) { + const { name } = entry + if (typeof entry.source !== 'string') continue // hosted elsewhere + listed.add(name) + const dir = `plugins/${name}` + if (entry.source !== `./${dir}`) errors.push(`${name}: source should be "./${dir}", not "${entry.source}"`) + if (!dirs.has(name)) { + errors.push(`${name}: listed in ${MARKETPLACE} but ${dir}/ does not exist`) + continue + } + if (entry.version !== undefined) errors.push(`${name}: remove "version" from the marketplace entry; plugin.json holds it`) + if (!entry.description) errors.push(`${name}: the marketplace entry needs a "description"`) + if (!entry.category) errors.push(`${name}: the marketplace entry needs a "category"`) + + const manifestPath = `${dir}/.claude-plugin/plugin.json` + if (!exists(manifestPath)) { + errors.push(`${name}: ${manifestPath} is missing`) + continue + } + const manifest = readJson(manifestPath) + if (manifest.name !== name) errors.push(`${name}: plugin.json name is "${manifest.name}"; it must match the entry and folder`) + if (!manifest.version) errors.push(`${name}: plugin.json needs a "version"`) + if (!exists(`${dir}/README.md`)) errors.push(`${name}: ${dir}/README.md is missing`) + if (!exists(`${dir}/CHANGELOG.md`)) { + errors.push(`${name}: ${dir}/CHANGELOG.md is missing`) + } else if (manifest.version && !read(`${dir}/CHANGELOG.md`).includes(`## [${manifest.version}]`)) { + errors.push(`${name}: ${dir}/CHANGELOG.md has no "## [${manifest.version}]" section`) + } + } + + for (const dir of dirs) { + if (!listed.has(dir)) errors.push(`plugins/${dir}/ has no entry in ${MARKETPLACE}`) + } + return errors +} + +const cell = (text: string) => text.replace(/\|/g, '\\|').replace(/\s*\n\s*/g, ' ') + +export function catalogTable(marketplace: Marketplace): string { + if (marketplace.plugins.length === 0) return 'Nothing published yet.' + const rows = marketplace.plugins.map(entry => { + const local = typeof entry.source === 'string' + const manifestPath = `plugins/${entry.name}/.claude-plugin/plugin.json` + const manifest = local && exists(manifestPath) ? readJson(manifestPath) : undefined + const title = entry.displayName ?? manifest?.displayName ?? entry.name + const link = local ? `[${title}](plugins/${entry.name})` : title + const description = entry.description ?? manifest?.description ?? '' + return `| ${link} | \`${entry.name}\` | ${entry.category ?? ''} | ${cell(description)} |` + }) + return ['| Plugin | Install id | Type | What it does |', '| --- | --- | --- | --- |', ...rows].join('\n') +} + +// The README with its catalog table replaced, or null when it has no markers. +export function renderReadme(marketplace: Marketplace): string | null { + const readme = read(README) + const start = readme.indexOf(START) + const end = readme.indexOf(END) + if (start === -1 || end < start) return null + return `${readme.slice(0, start + START.length)}\n${catalogTable(marketplace)}\n${readme.slice(end)}` +} + +export function updateReadme(marketplace: Marketplace): void { + const next = renderReadme(marketplace) + if (next === null) throw new Error(`${README} has no ${START} ... ${END} markers`) + write(README, next) +} + +export const isReadmeCurrent = (marketplace: Marketplace) => renderReadme(marketplace) === read(README) diff --git a/scripts/new-plugin.mts b/scripts/new-plugin.mts new file mode 100644 index 0000000..99ea751 --- /dev/null +++ b/scripts/new-plugin.mts @@ -0,0 +1,113 @@ +// Scaffolds plugins// from templates/, adds its marketplace entry and +// regenerates the README catalog. +// +// node --experimental-strip-types scripts/new-plugin.mts [options] +// +// --with components, comma separated: skill, agent, hooks, mcp, mod (default: skill) +// --description one line for the manifest, the marketplace and the README +// --category catalog type (default: from the first component) +// --display-name name shown in the UI (default: in title case) + +import { parseArgs } from 'node:util' + +import { MARKETPLACE, exists, read, readJson, updateReadme, write, writeJson } from './lib.mts' +import type { Marketplace } from './lib.mts' + +const COMPONENTS = { + skill: { category: 'skills', line: (n: string) => `- Skill \`/${n}:${n}\`: TODO` }, + agent: { category: 'agents', line: (n: string) => `- Subagent \`${n}:${n}\`: TODO` }, + hooks: { category: 'hooks', line: () => '- Hooks in `hooks/hooks.json`: TODO' }, + mcp: { category: 'mcp', line: () => '- MCP servers in `.mcp.json`: TODO' }, + mod: { category: 'mods', line: (n: string) => `- A mod with the command \`/${n}\`: TODO` }, +} as const +type Component = keyof typeof COMPONENTS + +const { values, positionals } = parseArgs({ + allowPositionals: true, + options: { + with: { type: 'string', default: 'skill' }, + description: { type: 'string', default: 'TODO: one line on what it does' }, + category: { type: 'string' }, + 'display-name': { type: 'string' }, + }, +}) + +function fail(message: string): never { + console.error(message) + process.exit(1) +} + +const name = positionals[0] ?? fail('usage: scripts/new-plugin.mts [--with skill,agent,hooks,mcp,mod] [--description ...]') +if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) fail(`"${name}" is not kebab-case`) +// claude plugin validate refuses names that pass as Anthropic's own. +if (/^(claude|anthropics?|cc-plugin)(-|$)/.test(name) || name === 'claude-code') fail(`"${name}" is reserved by Claude Code`) + +const dir = `plugins/${name}` +if (exists(dir)) fail(`${dir}/ already exists`) + +const components = values.with.split(',').map(c => c.trim()).filter(Boolean) +for (const c of components) if (!(c in COMPONENTS)) fail(`unknown component "${c}"; use ${Object.keys(COMPONENTS).join(', ')}`) +const chosen = components as Component[] +if (chosen.length === 0) fail('--with needs at least one component') + +const marketplace = readJson(MARKETPLACE) +if (marketplace.plugins.some(p => p.name === name)) fail(`${name} is already in ${MARKETPLACE}`) + +const displayName = values['display-name'] ?? name.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join(' ') +const description = values.description.replace(/\.$/, '') +const category = values.category ?? COMPONENTS[chosen[0]].category +const repo = 'https://github.com/Samarth2001/claude-plugins' + +const vars: Record = { + name, + displayName, + description, + date: new Date().toISOString().slice(0, 10), + components: chosen.map(c => COMPONENTS[c].line(name)).join('\n'), +} +const fill = (text: string) => text.replace(/\{\{(\w+)\}\}/g, (_, key: string) => vars[key] ?? fail(`no value for {{${key}}}`)) + +const emit = (from: string, to: string) => write(`${dir}/${to}`, fill(read(`templates/${from}`))) + +const manifest: Record = { + name, + displayName, + version: '0.1.0', + description, + author: marketplace.owner, + homepage: `${repo}/tree/main/${dir}`, + repository: repo, + license: 'MIT', + keywords: [], +} + +emit('plugin/README.md', 'README.md') +emit('plugin/CHANGELOG.md', 'CHANGELOG.md') +if (chosen.includes('skill')) emit('skill/SKILL.md', `skills/${name}/SKILL.md`) +if (chosen.includes('agent')) emit('agent/agent.md', `agents/${name}.md`) +if (chosen.includes('mcp')) writeJson(`${dir}/.mcp.json`, { mcpServers: {} }) +if (chosen.includes('mod')) { + emit('mod/register.ts', 'hooks/register.ts') + emit('mod/register.test.ts', 'hooks/register.test.ts') + write(`${dir}/tsconfig.json`, '{ "extends": "./.claude-plugin/types/tsconfig.json" }\n') +} +// A mod and settings hooks share hooks/hooks.json: `modules` for the mod, +// `hooks` for the event map. +if (chosen.includes('mod') || chosen.includes('hooks')) { + const hooks: Record = {} + if (chosen.includes('mod')) hooks.modules = ['./register.ts'] + if (chosen.includes('hooks')) hooks.hooks = {} + writeJson(`${dir}/hooks/hooks.json`, hooks) +} +writeJson(`${dir}/.claude-plugin/plugin.json`, manifest) + +marketplace.plugins.push({ name, source: `./${dir}`, description, category, tags: [...chosen] }) +writeJson(MARKETPLACE, marketplace) +updateReadme(marketplace) + +console.log(`Created ${dir}/ (${chosen.join(', ')}), added it to ${MARKETPLACE} and the README catalog. + +Next: + 1. Fill in the TODOs: grep -rn TODO ${dir} + 2. claude plugin validate ${dir}${chosen.includes('mod') ? `\n claude plugin test ${dir}` : ''} + 3. claude --plugin-dir ${dir}`) diff --git a/templates/agent/agent.md b/templates/agent/agent.md new file mode 100644 index 0000000..d25d8fb --- /dev/null +++ b/templates/agent/agent.md @@ -0,0 +1,11 @@ +--- +name: {{name}} +description: {{description}}. Use when TODO. +--- + +You are TODO. When invoked: + +1. TODO +2. TODO + +Report back with TODO. diff --git a/templates/mod/register.test.ts b/templates/mod/register.test.ts new file mode 100644 index 0000000..9526ae3 --- /dev/null +++ b/templates/mod/register.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'claude-code/testing' + +test('/{{name}} answers', async $ => { + const answer = await $.command.run({ command: '{{name}}', args: '' }) + expect(answer.text).toBe('Hello from {{name}}') +}) diff --git a/templates/mod/register.ts b/templates/mod/register.ts new file mode 100644 index 0000000..964ef24 --- /dev/null +++ b/templates/mod/register.ts @@ -0,0 +1,12 @@ +import type { Register } from 'claude-code' + +export const register: Register = on => { + on('session.start', async ($, e, next) => { + await $.command.register({ name: '{{name}}', description: '{{description}}' }) + return next(e) + }) + + on('command.run', { command: '{{name}}' }, async () => { + return { text: 'Hello from {{name}}' } + }) +} diff --git a/templates/plugin/CHANGELOG.md b/templates/plugin/CHANGELOG.md new file mode 100644 index 0000000..2e3b6c8 --- /dev/null +++ b/templates/plugin/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to {{name}}. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versions follow [Semantic Versioning](https://semver.org/). Each release is tagged `{{name}}--v`. + +## [0.1.0] - {{date}} + +### Added + +- First release. diff --git a/templates/plugin/README.md b/templates/plugin/README.md new file mode 100644 index 0000000..d3cce20 --- /dev/null +++ b/templates/plugin/README.md @@ -0,0 +1,35 @@ +# {{displayName}} + +{{description}} + +## Install + +```bash +claude plugin marketplace add Samarth2001/claude-plugins +claude plugin install {{name}}@samarth +``` + +Then run `/reload-plugins` in an open session, or start a new one. + +## What it adds + +{{components}} + +## Usage + +TODO: how to use it, with an example. + +## Configuration + +None. + +## Develop + +From the repository root: + +```bash +claude plugin validate plugins/{{name}} +claude --plugin-dir plugins/{{name}} # a session with it loaded +``` + +See [CHANGELOG.md](CHANGELOG.md) for releases. diff --git a/templates/skill/SKILL.md b/templates/skill/SKILL.md new file mode 100644 index 0000000..1a3b5de --- /dev/null +++ b/templates/skill/SKILL.md @@ -0,0 +1,9 @@ +--- +name: {{name}} +description: {{description}}. Use when TODO (the words a user would say). +--- + +# {{displayName}} + +TODO: what Claude should do, as short numbered steps or rules. State what to do, not why. +Keep this file under 500 lines; move long reference material into files beside it and link them. From d1bc1744929d6ac9cb26fb14c3edae35eefaa16f Mon Sep 17 00:00:00 2001 From: Samarth Rayar Date: Fri, 2 Oct 2026 07:39:13 +0000 Subject: [PATCH 3/3] Neutral front page, guides and contributor docs The root README describes the marketplace as a whole, with a generated catalog instead of a buddy hero. Adds docs/ guides on plugin anatomy and writing skills, a CLAUDE.md with repo rules, /new-plugin and /release project skills, and a PR template. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QUAMWfzunRcEdkbuZNaq9F --- .claude-plugin/marketplace.json | 2 +- .claude/skills/new-plugin/SKILL.md | 13 +++++ .claude/skills/release/SKILL.md | 14 +++++ .github/pull_request_template.md | 9 +++ CLAUDE.md | 19 ++++++ CONTRIBUTING.md | 71 +++++++++++++---------- README.md | 63 +++++++++++--------- docs/plugin-anatomy.md | 92 ++++++++++++++++++++++++++++++ docs/writing-skills.md | 47 +++++++++++++++ 9 files changed, 271 insertions(+), 59 deletions(-) create mode 100644 .claude/skills/new-plugin/SKILL.md create mode 100644 .claude/skills/release/SKILL.md create mode 100644 .github/pull_request_template.md create mode 100644 CLAUDE.md create mode 100644 docs/plugin-anatomy.md create mode 100644 docs/writing-skills.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 15ef978..8f7cce2 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,6 +1,6 @@ { "name": "samarth", - "description": "Samarth Rayar's Claude Code plugins, mods and skills", + "description": "Samarth Rayar's Claude Code plugins: mods, skills, subagents, hooks and MCP servers", "owner": { "name": "Samarth Rayar", "url": "https://github.com/Samarth2001" }, "plugins": [ { diff --git a/.claude/skills/new-plugin/SKILL.md b/.claude/skills/new-plugin/SKILL.md new file mode 100644 index 0000000..496bbe2 --- /dev/null +++ b/.claude/skills/new-plugin/SKILL.md @@ -0,0 +1,13 @@ +--- +name: new-plugin +description: Scaffold a new plugin in this marketplace. Use when asked to add, create or start a new plugin, skill bundle, mod, subagent, hook or MCP server in this repo. +argument-hint: " [what it should do]" +--- + +Create a new plugin from: $ARGUMENTS + +1. Pick a kebab-case name and the components it needs (`skill`, `agent`, `hooks`, `mcp`, `mod`). Read `docs/plugin-anatomy.md` to choose. If the name or purpose is unclear, ask. +2. Run `node --experimental-strip-types scripts/new-plugin.mts --with --description ""`. +3. Fill in every TODO in `plugins//`. For a skill, follow `docs/writing-skills.md`. For a mod, load the `plugin-authoring` skill first. +4. Write the plugin README: what it adds, usage with an example, configuration, and what code it runs. +5. Run `claude plugin validate plugins/`, `claude plugin test plugins/` for a mod, `claude plugin validate .` and `node --experimental-strip-types scripts/catalog.mts --check`. Fix anything they report. diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..74da7df --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,14 @@ +--- +name: release +description: Release a new version of a plugin in this marketplace. +disable-model-invocation: true +argument-hint: " [patch|minor|major]" +--- + +Release plugin: $ARGUMENTS + +1. Read `plugins//CHANGELOG.md` and run `git log --v..HEAD -- plugins/`. If that tag doesn't exist, use the full log for the folder. Then list what changed. +2. Choose the bump if none was given: patch for fixes, minor for features, major for breaking changes such as renamed commands or settings. Confirm it with me. +3. Set `version` in `plugins//.claude-plugin/plugin.json`. Add a `## [] - ` section to the CHANGELOG under Added, Changed or Fixed, plus its compare link at the bottom. +4. Run `claude plugin validate .`, `claude plugin validate plugins/`, `claude plugin test plugins/` if it has tests, and `node --experimental-strip-types scripts/catalog.mts --check`. +5. Commit as `: release `. Then stop and show me the commands to push and tag: `git push` and `claude plugin tag plugins/ --push`. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d90d1e4 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,9 @@ +## What changed + +## Plugins touched + +- [ ] Version bumped in `plugin.json` and a CHANGELOG section added for each plugin whose shipped files changed +- [ ] The plugin's README is updated for anything users see +- [ ] `claude plugin validate .` and `claude plugin validate plugins/` pass +- [ ] `claude plugin test plugins/` passes (mods) +- [ ] `node --experimental-strip-types scripts/catalog.mts --check` passes diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e15c7cb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,19 @@ +# claude-plugins + +A Claude Code plugin marketplace (`samarth`). Each plugin lives in `plugins//`. Read CONTRIBUTING.md before changing structure or releasing. + +## Rules + +- Keep the root README neutral. Never feature one plugin there. Its catalog table is generated: edit `.claude-plugin/marketplace.json`, then run `node --experimental-strip-types scripts/catalog.mts`. +- Keep each plugin self-contained, in Claude Code's standard layout, with its own README.md and CHANGELOG.md. Dev-only tooling goes in `scripts/`, never inside a plugin. +- Never rename a plugin's `name`. Leave `version` out of marketplace entries. +- Any change to a plugin's shipped files needs a version bump and a CHANGELOG section. README, CHANGELOG, `assets/` and tests are exempt. +- Before reporting work done, run `claude plugin validate .`, `claude plugin validate plugins/`, `claude plugin test plugins/` (if it has tests) and `node --experimental-strip-types scripts/catalog.mts --check`. +- Before writing a mod, load the `plugin-authoring` skill. +- Guides in `docs/` summarize the official docs at code.claude.com. Link to the source and don't state behavior you haven't checked. + +## Commands + +- New plugin: `node --experimental-strip-types scripts/new-plugin.mts --with skill,agent,hooks,mcp,mod --description "..."` +- Regenerate assets: `node --experimental-strip-types scripts/assets/