From 0876e6bb5737dcecd7095a4359e16a3fcc89c62b Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 18 Sep 2026 14:58:26 -0600 Subject: [PATCH] scaffold community skills repo with plugin harness, categories, and CI --- .agents/plugins/marketplace.json | 20 +++++ .claude-plugin/marketplace.json | 15 ++++ .claude-plugin/plugin.json | 8 ++ .codex-plugin/plugin.json | 6 ++ .cursor-plugin/marketplace.json | 13 +++ .cursor-plugin/plugin.json | 13 +++ .github/dependabot.yml | 7 ++ .github/workflows/ci.yml | 37 +++++++++ .gitignore | 4 + .markdownlint.jsonc | 7 ++ CODEOWNERS | 3 + CONTRIBUTING.md | 50 ++++++++++++ GO-PUBLIC.sh | 36 +++++++++ README.md | 132 ++++++++++++++++++++++++++++++- SECURITY.md | 13 +++ plugin.json | 21 +++++ scripts/check-internal-links.mjs | 64 +++++++++++++++ scripts/verify-evals.mjs | 80 +++++++++++++++++++ scripts/verify-manifests.mjs | 111 ++++++++++++++++++++++++++ scripts/verify-skills.mjs | 85 ++++++++++++++++++++ skills/README.md | 13 +++ skills/db/README.md | 6 ++ skills/design/README.md | 6 ++ skills/devops/README.md | 6 ++ skills/skills/README.md | 5 ++ skills/tools/README.md | 6 ++ 26 files changed, 766 insertions(+), 1 deletion(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .codex-plugin/plugin.json create mode 100644 .cursor-plugin/marketplace.json create mode 100644 .cursor-plugin/plugin.json create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .markdownlint.jsonc create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100755 GO-PUBLIC.sh create mode 100644 SECURITY.md create mode 100644 plugin.json create mode 100644 scripts/check-internal-links.mjs create mode 100644 scripts/verify-evals.mjs create mode 100644 scripts/verify-manifests.mjs create mode 100644 scripts/verify-skills.mjs create mode 100644 skills/README.md create mode 100644 skills/db/README.md create mode 100644 skills/design/README.md create mode 100644 skills/devops/README.md create mode 100644 skills/skills/README.md create mode 100644 skills/tools/README.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..65181c4 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "name": "community-skills", + "interface": { + "displayName": "Community Skills" + }, + "plugins": [ + { + "name": "community-skills", + "source": { + "source": "url", + "url": "https://github.com/ForgeUtah/Skills.git" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Developer Tools" + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..7a8cdd9 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://code.claude.com/schemas/marketplace.json", + "name": "community-skills", + "owner": { + "name": "Community Skills Contributors", + "url": "https://github.com/ForgeUtah/Skills" + }, + "plugins": [ + { + "name": "community-skills", + "source": "./", + "description": "Community-contributed Agent Skills across categories: DevOps, design, databases, tools, and general skills." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..0e2da3f --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "community-skills", + "description": "Community-contributed Agent Skills across categories: DevOps, design, databases, tools, and general skills.", + "version": "0.1.0", + "author": { + "name": "Community Skills Contributors" + } +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 0000000..533eecf --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "community-skills", + "version": "0.1.0", + "description": "Community-contributed Agent Skills across categories: DevOps, design, databases, tools, and general skills.", + "skills": "./skills/" +} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..4e23c53 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,13 @@ +{ + "name": "community-skills", + "owner": { + "name": "Community Skills Contributors" + }, + "plugins": [ + { + "name": "community-skills", + "source": "./", + "description": "Community-contributed Agent Skills across categories: DevOps, design, databases, tools, and general skills." + } + ] +} diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json new file mode 100644 index 0000000..c9a0628 --- /dev/null +++ b/.cursor-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "community-skills", + "version": "0.1.0", + "description": "Community-contributed Agent Skills across categories: DevOps, design, databases, tools, and general skills.", + "author": { + "name": "Community Skills Contributors" + }, + "homepage": "https://github.com/ForgeUtah/Skills", + "repository": "https://github.com/ForgeUtah/Skills", + "license": "MIT", + "keywords": ["community-skills", "devops", "design", "database", "tools", "agents", "skills"], + "skills": "./skills/" +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1705d1b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3313973 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI +on: + pull_request: {} + push: + branches: [main] + workflow_dispatch: {} + +concurrency: + group: ci-${{ github.event_name }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + verify: + name: verify-skills-repo + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 1 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Lint skill frontmatter and required sections + run: node scripts/verify-skills.mjs + - name: Validate plugin manifests against their schema shape + run: node scripts/verify-manifests.mjs + - name: Validate portable skill eval fixtures + run: node scripts/verify-evals.mjs + - name: Fail on internal markdown links to missing files + run: node scripts/check-internal-links.mjs + - name: Lint markdown files + run: npx --yes markdownlint-cli2 --config .markdownlint.jsonc "**/*.md" "!node_modules" + - name: Lint JavaScript scripts + run: npx oxlint@latest scripts/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..612c939 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +coverage/ +*.log +.DS_Store diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..8a038d9 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,7 @@ +{ + "default": true, + "MD013": false, + "MD033": false, + "MD041": false, + "MD060": false +} diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..80bdb18 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ +# Every change requires review from a code owner before it can merge to main. +# main is protected: no direct pushes, PRs only. +* @Soypete @clintberry diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..d0ad270 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +Thank you for contributing a skill to the community repo! + +## Ground rules + +- **Skills document existing behavior.** If a skill names a command, endpoint, + flag, or type that does not exist, the code wins. Update the skill. +- **Prefer retrieval over pre-training.** Point agents at the authoritative + source (docs, READMEs, code) rather than duplicating details that can drift. +- **No absolute local paths.** This repo is public. Never reference a personal + or machine-specific path. Use repo-relative paths and product names only. +- **No screenshots.** Reference a live resource instead. +- **Every skill carries two sections** in its `SKILL.md`: + `## Validation commands` (how a user verifies the guidance) and + `## Realistic usage boundaries` (what the skill does not cover). +- **Every skill needs eval cases** in `evals/evals.json` — at least two + prompt-only, portable cases with expectations. + +## Adding or changing a skill + +1. Read the source of truth for the subject matter. Verify every command, + endpoint, and flag against the code. +2. Place your skill in the right category under `skills/`: + - `skills/skills/` — meta-skills, skill development + - `skills/devops/` — CI/CD, infrastructure, deployment + - `skills/design/` — UI/UX, design systems, frontend + - `skills/db/` — SQL, NoSQL, data modeling + - `skills/tools/` — general-purpose development tools + - If none of these fit, add a new top-level directory under `skills/`. +3. Create `SKILL.md` with `name` (lowercase-hyphen, matching folder name) and + `description` frontmatter. Keep it roughly 130-340 lines. +4. Add at least two portable eval cases in `evals/evals.json`. +5. Add a row to the Skills table in `README.md`. +6. Run the verification suite before committing: + + ```bash + node scripts/verify-skills.mjs + node scripts/verify-manifests.mjs + node scripts/check-internal-links.mjs + node scripts/verify-evals.mjs + ``` + +## Licensing + +This repository is **MIT** licensed. Do not add third-party code or content +that is not MIT-compatible. + +`main` is protected — no direct pushes. Every change lands through a pull +request approved by a code owner (see `CODEOWNERS`). diff --git a/GO-PUBLIC.sh b/GO-PUBLIC.sh new file mode 100755 index 0000000..f8e720e --- /dev/null +++ b/GO-PUBLIC.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Flip the community skills repo to public and lock main down. +# +# Run this when you and Clint have reviewed the repo on GitHub and are happy +# for it to be public. +set -euo pipefail + +REPO=ForgeUtah/Skills + +echo "==> 1/3 Making $REPO public" +gh repo edit "$REPO" --visibility public --accept-visibility-change-consequences + +echo "==> 2/3 Protecting main (no direct pushes, PR + code owner review)" +gh api -X PUT "repos/$REPO/branches/main/protection" --input - <<'JSON' +{ + "required_status_checks": null, + "enforce_admins": false, + "required_pull_request_reviews": { + "required_approving_review_count": 1, + "require_code_owner_reviews": true, + "dismiss_stale_reviews": true + }, + "restrictions": null, + "allow_force_pushes": false, + "allow_deletions": false +} +JSON + +echo "==> 3/3 Verifying" +gh repo view "$REPO" --json visibility,licenseInfo \ + --jq '"visibility: \(.visibility) licence: \(.licenseInfo.name)"' +gh api "repos/$REPO/branches/main/protection" \ + --jq '"code owner review required: \(.required_pull_request_reviews.require_code_owner_reviews) force pushes: \(.allow_force_pushes.enabled)"' + +echo +echo "Done. main now requires a PR approved by @Soypete or @clintberry." diff --git a/README.md b/README.md index 9a4cab1..5bd56a7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,132 @@ # Skills -Community built skills + +Community built skills. See [CONTRIBUTING.md](CONTRIBUTING.md) and the +category directories under [`skills/`](skills/) to get started. + +A collection of [Agent Skills](https://github.com/agentskills/agentskills) contributed by the +community. These skills are portable across Claude Code, Cursor, opencode, Pi, +and Codex — write once, use in any coding agent that supports the Agent Skills +format. + +Skills are organized by category: + +| Category | Directory | What you'll find | +|----------|-----------|------------------| +| Skills | [`skills/skills/`](skills/skills/) | Meta-skills, skill development, and agent skill tooling | +| DevOps | [`skills/devops/`](skills/devops/) | CI/CD, infrastructure, deployment, platform tooling | +| Design | [`skills/design/`](skills/design/) | UI/UX, design systems, frontend architecture | +| Databases | [`skills/db/`](skills/db/) | SQL, NoSQL, data modeling, migrations | +| Tools | [`skills/tools/`](skills/tools/) | General-purpose development tools and utilities | + +## Installing + +### Copyable setup prompt + +Paste this prompt into the coding agent you want to configure: + +```text +Install the Community Skills from https://github.com/ForgeUtah/Skills.git. +First inspect which harness you are running (Claude Code, Cursor, OpenCode, +Pi, or Codex) and its documented user/project skill directory. Clone the +repository to a temporary directory, review the skills/ folders, then copy or +symlink each folder containing SKILL.md into that harness's skill directory. +Preserve existing skills, do not overwrite a same-named skill without asking, +and do not copy .git metadata. After installation, verify that all SKILL.md +files have name and description frontmatter and report the destination and +the installed skill names. Do not print secrets or modify project files +outside the selected skill directory. +``` + +### Claude Code + +Install from the plugin marketplace: + +```text +/plugin marketplace add ForgeUtah/Skills +/plugin install community-skills@community-skills +``` + +### Codex + +```text +codex plugin marketplace add ForgeUtah/Skills +codex plugin add community-skills@community-skills +``` + +### opencode + +Clone this repo and symlink or copy the skill folders into +`~/.config/opencode/skills/` (or the project's `.opencode/skills/`). + +### Pi + +Pi loads the same Agent Skills directories. Clone this repo and either add +the repository's `skills/` directory to `~/.pi/agent/skills/`, or configure +the clone in Pi's settings: + +```json +{ + "skills": ["/path/to/community-skills/skills"] +} +``` + +For a project-local install, use `.pi/skills/` or configure the repository's +`skills/` directory in `.pi/settings.json`. Pi also discovers the skills +through `~/.agents/skills/` and project `.agents/skills/` directories. + +### Cursor + +Add via **Settings > Rules > Add Rule > Remote Rule (GitHub)** with +`ForgeUtah/Skills`, or copy the skill folders into `~/.cursor/skills/`. + +### Clone / Copy + +Clone this repo and copy the skill folders into the appropriate directory for +your agent: + +| Agent | Skill Directory | Docs | +|-------|-----------------|------| +| Claude Code | `~/.claude/skills/` | [docs](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview) | +| Cursor | `~/.cursor/skills/` | [docs](https://cursor.com/docs/context/skills) | +| opencode | `~/.config/opencode/skills/` | [docs](https://opencode.ai/docs/skills/) | +| Pi | `~/.pi/agent/skills/` or `~/.agents/skills/` | [docs](https://pi.dev/docs/latest/skills) | +| Codex | `~/.codex/skills/` | [docs](https://developers.openai.com/codex/skills/) | + +## Contributing a skill + +See [CONTRIBUTING.md](CONTRIBUTING.md). I use the +[OpenAI Skill Creator](https://developers.openai.com/plugins/build/skills) to +scaffold new skills — it generates the correct frontmatter, sections, and eval +fixtures. Short version: + +1. Skills document existing behavior — they never invent APIs. +2. Place your skill in the matching category under `skills/`. +3. Every skill needs a `SKILL.md` with `name` and `description` frontmatter. +4. Add at least two eval cases in `evals/evals.json`. +5. Run the verification suite before opening a PR. +6. If no existing category fits, add a new top-level directory under `skills/`. + +## What makes a good skill + +- **Focused scope** — one skill should cover one product, tool, or domain. +- **Retrieval-first** — point agents at authoritative docs rather than + duplicating details that can drift. +- **No absolute paths** — reference repos by product name and relative paths. +- **Portable** — work in any harness that supports Agent Skills. +- **Tested** — each skill carries eval cases so contributors can validate changes. + +## Validation + +The suite is self-checking and enforced by CI (`.github/workflows/ci.yml`) +on every pull request and push to `main`. Run before opening a PR: + +```bash +node scripts/verify-skills.mjs # frontmatter, name=dir, required sections +node scripts/verify-manifests.mjs # every plugin manifest parses as JSON +node scripts/check-internal-links.mjs # every internal markdown link resolves +node scripts/verify-evals.mjs # every skill has eval cases +``` + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f9cefb3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +Do not report security issues in public issues. Until a dedicated security +contact is configured, contact the repository owners privately through GitHub: +`@Soypete` or `@colinso`. + +Do not include secrets, access tokens, private endpoints, personal data, or +production identifiers in reports or pull requests. + +Skills must fail closed when they encounter missing authorization, ambiguous +scope, malformed tool input, or unavailable dependencies. They must not direct +an agent to bypass access controls, expose credentials, or persist raw private +content. diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..b74d3bf --- /dev/null +++ b/plugin.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "community-skills", + "version": "0.1.0", + "description": "Community-contributed Agent Skills across categories: DevOps, design, databases, tools, and general skills. Skills are portable across Claude Code, Cursor, opencode, Pi, and Codex.", + "author": { + "name": "Community Skills Contributors" + }, + "homepage": "https://github.com/ForgeUtah/Skills", + "repository": "https://github.com/ForgeUtah/Skills", + "license": "MIT", + "keywords": [ + "community-skills", + "devops", + "design", + "database", + "tools", + "agents", + "skills" + ] +} diff --git a/scripts/check-internal-links.mjs b/scripts/check-internal-links.mjs new file mode 100644 index 0000000..f221bea --- /dev/null +++ b/scripts/check-internal-links.mjs @@ -0,0 +1,64 @@ +/** + * Fails on internal markdown links that point at missing files. + * + * Scans every .md file in the repo (excluding node_modules), extracts + * [text](target) and [text](target "title") markdown links, and resolves + * targets that are relative paths (not http/https/mailto, not bare anchors, + * not inline code). A target that is a relative path whose resolved file or + * directory does not exist is a hard failure. + * + * Exits non-zero on the first problem and prints a stable summary line on success. + */ + +import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, relative, resolve, extname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import process from 'node:process'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +// [text](target) or [text](target "title") or [text](target 'title') or [text](target (title)) +// Captures group 1 = target. Not matched inside code spans (a very small heuristic: +// this is a lint, not a parser). +const LINK_RE = /\[[^\]]*\]\(([^)\s]+)(?:\s+["'(][^"')]*["')])?\)/g; + +function walk(root) { + const out = []; + const entries = readdirSync(root); + for (const entry of entries) { + if (entry === 'node_modules' || entry === '.git') continue; + const full = join(root, entry); + const st = statSync(full); + if (st.isDirectory()) { + out.push(...walk(full)); + } else if (extname(entry) === '.md') { + out.push(full); + } + } + return out; +} + +const mdFiles = walk(ROOT); +let linkCount = 0; + +for (const file of mdFiles) { + const raw = readFileSync(file, 'utf8'); + for (const match of raw.matchAll(LINK_RE)) { + const target = match[1]; + if ( + /^[a-z][a-z0-9+.-]*:/i.test(target) || // scheme (http, https, mailto, ...) + target.startsWith('#') // in-file anchor + ) { + continue; + } + linkCount += 1; + // Strip any trailing "| something" or " {text}" link-reference syntax. + const resolved = resolve(dirname(file), target.split('|')[0].trim()); + if (!existsSync(resolved)) { + process.stderr.write(`check-internal-links: broken link in ${relative(ROOT, file)} -> ${target}\n`); + process.exit(1); + } + } +} + +process.stdout.write(`links verified: ${linkCount} internal links across ${mdFiles.length} markdown files resolve\n`); diff --git a/scripts/verify-evals.mjs b/scripts/verify-evals.mjs new file mode 100644 index 0000000..0510e93 --- /dev/null +++ b/scripts/verify-evals.mjs @@ -0,0 +1,80 @@ +import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import process from 'node:process'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const SKILL_ROOT = join(ROOT, 'skills'); + +function fail(message) { + process.stderr.write(`verify-evals: ${message}\n`); + process.exit(1); +} + +function listSkillDirs(root) { + const out = []; + let topEntries; + try { + topEntries = readdirSync(root); + } catch { + return out; + } + for (const entry of topEntries.sort()) { + if (entry.startsWith('.')) continue; + const full = join(root, entry); + if (!statSync(full).isDirectory()) continue; + const subEntries = readdirSync(full); + for (const sub of subEntries.sort()) { + if (sub.startsWith('.')) continue; + const subFull = join(full, sub); + if (statSync(subFull).isDirectory()) { + out.push({ category: entry, name: sub, path: subFull }); + } + } + } + return out; +} + +const skills = listSkillDirs(SKILL_ROOT); +if (skills.length === 0) { + process.stdout.write('verify-evals: no skills yet — add evals/evals.json with each new skill\n'); + process.exit(0); +} + +for (const { category, name, path } of skills) { + const evalsPath = join(path, 'evals', 'evals.json'); + if (!existsSync(evalsPath)) fail(`${category}/${name}: missing evals/evals.json`); + + let document; + try { + document = JSON.parse(readFileSync(evalsPath, 'utf8')); + } catch (error) { + fail(`${category}/${name}: invalid JSON (${error.message})`); + } + + if (document.skill_name !== name) { + fail(`${category}/${name}: skill_name must match the folder name`); + } + if (!Array.isArray(document.evals) || document.evals.length < 2) { + fail(`${category}/${name}: evals must contain at least two cases`); + } + + const ids = new Set(); + for (const test of document.evals) { + if (!Number.isInteger(test.id) || ids.has(test.id)) { + fail(`${category}/${name}: eval ids must be unique integers`); + } + ids.add(test.id); + if (typeof test.prompt !== 'string' || test.prompt.trim() === '') { + fail(`${category}/${name}: every eval needs a prompt`); + } + if (typeof test.expected_output !== 'string' || test.expected_output.trim() === '') { + fail(`${category}/${name}: every eval needs expected_output`); + } + if (!Array.isArray(test.expectations) || test.expectations.length === 0) { + fail(`${category}/${name}: every eval needs one or more expectations`); + } + } +} + +process.stdout.write(`eval fixtures verified: ${skills.length} skills\n`); diff --git a/scripts/verify-manifests.mjs b/scripts/verify-manifests.mjs new file mode 100644 index 0000000..f8a3078 --- /dev/null +++ b/scripts/verify-manifests.mjs @@ -0,0 +1,111 @@ +/** + * Validates every plugin manifest in the repo: parses as JSON and carries the + * required shape for its declared schema. + * + * Manifests checked: + * - plugin.json (agent-plugins.org schema 1.0.0) + * - .claude-plugin/plugin.json + marketplace.json + * - .codex-plugin/plugin.json + * - .cursor-plugin/plugin.json + marketplace.json + * - .agents/plugins/marketplace.json + * + * Exits non-zero on the first problem and prints a stable summary line on success. + */ + +import { readFileSync, existsSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import process from 'node:process'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +// Each manifest: relative path -> predicate on the parsed object. +// Predicates mirror the shape each schema expects. Keep them strict but +// additive: this is a shape lint, not a full schema validator. +const MANIFESTS = [ + { + path: 'plugin.json', + check: (o) => + typeof o['$schema'] === 'string' && + o['$schema'].startsWith('https://agent-plugins.org/schemas/') && + typeof o.name === 'string' && + o.name !== '' && + typeof o.version === 'string' && + typeof o.description === 'string' && + typeof o.license === 'string' && + Array.isArray(o.keywords), + }, + { + path: '.claude-plugin/plugin.json', + check: (o) => + typeof o.name === 'string' && o.name !== '' && + typeof o.version === 'string' && + typeof o.description === 'string' && + typeof o.author?.name === 'string', + }, + { + path: '.claude-plugin/marketplace.json', + check: (o) => + typeof o['$schema'] === 'string' && + typeof o.name === 'string' && + typeof o.owner?.name === 'string' && + Array.isArray(o.plugins) && + o.plugins.every((p) => typeof p.name === 'string' && typeof p.source === 'string'), + }, + { + path: '.codex-plugin/plugin.json', + check: (o) => + typeof o.name === 'string' && o.name !== '' && + typeof o.version === 'string' && + typeof o.description === 'string' && + typeof o.skills === 'string', + }, + { + path: '.cursor-plugin/plugin.json', + check: (o) => + typeof o.name === 'string' && o.name !== '' && + typeof o.version === 'string' && + typeof o.description === 'string' && + typeof o.author?.name === 'string' && + typeof o.skills === 'string', + }, + { + path: '.cursor-plugin/marketplace.json', + check: (o) => + typeof o.name === 'string' && + typeof o.owner?.name === 'string' && + Array.isArray(o.plugins) && + o.plugins.every((p) => typeof p.name === 'string' && typeof p.source === 'string'), + }, + { + path: '.agents/plugins/marketplace.json', + check: (o) => + typeof o.name === 'string' && + typeof o.interface?.displayName === 'string' && + Array.isArray(o.plugins) && + o.plugins.every((p) => typeof p.name === 'string' && typeof p.source?.source === 'string'), + }, +]; + +let checked = 0; +for (const { path, check } of MANIFESTS) { + const full = join(ROOT, path); + if (!existsSync(full)) { + process.stderr.write(`verify-manifests: missing manifest ${path}\n`); + process.exit(1); + } + let parsed; + try { + parsed = JSON.parse(readFileSync(full, 'utf8')); + } catch (err) { + process.stderr.write(`verify-manifests: ${path} is not valid JSON: ${err.message}\n`); + process.exit(1); + } + if (!check(parsed)) { + process.stderr.write(`verify-manifests: ${path} does not match its declared schema shape\n`); + process.exit(1); + } + checked += 1; +} + +process.stdout.write(`manifests verified: ${checked} manifests match their declared schema shape\n`); diff --git a/scripts/verify-skills.mjs b/scripts/verify-skills.mjs new file mode 100644 index 0000000..f51118d --- /dev/null +++ b/scripts/verify-skills.mjs @@ -0,0 +1,85 @@ +import { readFileSync, readdirSync, statSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import process from 'node:process'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const SKILL_ROOT = join(ROOT, 'skills'); + +const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/; +const NAME_PATTERN = /^[a-z][a-z0-9-]{0,63}$/; +const PLACEHOLDERS = /\b(TODO|FIXME|XXX|PLACEHOLDER|REPLACE_ME)\b|lorem\s+ipsum/i; + +function parseFrontmatter(raw) { + const match = FRONTMATTER.exec(raw); + if (!match) return null; + const fields = {}; + for (const line of match[1].split('\n')) { + const idx = line.indexOf(':'); + if (idx <= 0) continue; + fields[line.slice(0, idx).trim()] = line.slice(idx + 1).trim(); + } + return fields; +} + +function listSkillDirs(root) { + const out = []; + let topEntries; + try { + topEntries = readdirSync(root); + } catch { + return out; + } + for (const entry of topEntries.sort()) { + if (entry.startsWith('.')) continue; + const full = join(root, entry); + if (!statSync(full).isDirectory()) continue; + const subEntries = readdirSync(full); + for (const sub of subEntries.sort()) { + if (sub.startsWith('.')) continue; + const subFull = join(full, sub); + if (statSync(subFull).isDirectory()) { + out.push({ category: entry, name: sub, path: subFull }); + } + } + } + return out; +} + +const skills = listSkillDirs(SKILL_ROOT); +if (skills.length === 0) { + process.stdout.write(`verify-skills: no skills yet — add one under skills///\n`); + process.exit(0); +} + +for (const { category, name, path } of skills) { + const skillFile = join(path, 'SKILL.md'); + const raw = readFileSync(skillFile, 'utf8'); + + const fields = parseFrontmatter(raw); + if (fields === null) { + throw new Error(`${category}/${name}/SKILL.md: missing frontmatter (needs a --- name/description block)`); + } + if (typeof fields.name !== 'string' || fields.name === '') { + throw new Error(`${category}/${name}/SKILL.md: frontmatter 'name' is required`); + } + if (typeof fields.description !== 'string' || fields.description === '') { + throw new Error(`${category}/${name}/SKILL.md: frontmatter 'description' is required`); + } + if (!NAME_PATTERN.test(fields.name)) { + throw new Error(`${category}/${name}/SKILL.md: name '${fields.name}' must be lowercase-hyphenated (a-z0-9-)`); + } + if (fields.name !== name) { + throw new Error(`${category}/${name}/SKILL.md: frontmatter name '${fields.name}' must match the folder name`); + } + + const body = raw.replace(FRONTMATTER, '').trim(); + if (body.length < 200) { + throw new Error(`${category}/${name}/SKILL.md: body is too short to be useful (${body.length} chars)`); + } + if (PLACEHOLDERS.test(body)) { + throw new Error(`${category}/${name}/SKILL.md: placeholder tokens (TODO/FIXME/lorem/placeholder) are not allowed`); + } +} + +process.stdout.write(`skills verified: ${skills.length} skills across ${new Set(skills.map(s => s.category)).size} categories\n`); diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..b25d723 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,13 @@ +# Skills + +Skills are organized by category. Each subdirectory is a category; each +child directory within a category is one distributable skill with a required +`SKILL.md`. + +| Category | Directory | Description | +|----------|-----------|-------------| +| General | [`skills/`](skills/) | Meta-skills, skill development, and agent skill tooling | +| DevOps | [`devops/`](devops/) | CI/CD, infrastructure, deployment, platform tooling | +| Design | [`design/`](design/) | UI/UX, design systems, frontend architecture | +| Databases | [`db/`](db/) | SQL, NoSQL, data modeling, migrations | +| Tools | [`tools/`](tools/) | General-purpose development tools and utilities | diff --git a/skills/db/README.md b/skills/db/README.md new file mode 100644 index 0000000..0d06fbc --- /dev/null +++ b/skills/db/README.md @@ -0,0 +1,6 @@ +# Database Skills + +SQL, NoSQL, data modeling, migrations, query optimization, and data +engineering. + +Add your skill as a subdirectory with a `SKILL.md` and `evals/evals.json`. diff --git a/skills/design/README.md b/skills/design/README.md new file mode 100644 index 0000000..6d73642 --- /dev/null +++ b/skills/design/README.md @@ -0,0 +1,6 @@ +# Design Skills + +UI/UX, design systems, component libraries, frontend architecture, and +accessibility. + +Add your skill as a subdirectory with a `SKILL.md` and `evals/evals.json`. diff --git a/skills/devops/README.md b/skills/devops/README.md new file mode 100644 index 0000000..f159ea7 --- /dev/null +++ b/skills/devops/README.md @@ -0,0 +1,6 @@ +# DevOps Skills + +CI/CD, infrastructure as code, deployment, containers, observability, and +platform tooling. + +Add your skill as a subdirectory with a `SKILL.md` and `evals/evals.json`. diff --git a/skills/skills/README.md b/skills/skills/README.md new file mode 100644 index 0000000..a2a6738 --- /dev/null +++ b/skills/skills/README.md @@ -0,0 +1,5 @@ +# General Skills + +Skills about skill development, meta-skills, and agent skill tooling. + +Add your skill as a subdirectory with a `SKILL.md` and `evals/evals.json`. diff --git a/skills/tools/README.md b/skills/tools/README.md new file mode 100644 index 0000000..ed6888a --- /dev/null +++ b/skills/tools/README.md @@ -0,0 +1,6 @@ +# Tools Skills + +General-purpose development tools: editors, build systems, package managers, +debuggers, testing frameworks, and utilities. + +Add your skill as a subdirectory with a `SKILL.md` and `evals/evals.json`.