From 89ecc5efa3053780214b2d0942627f9fca3b0ecf Mon Sep 17 00:00:00 2001 From: Jonathan Tsai Date: Fri, 23 Jan 2026 00:40:04 -0800 Subject: [PATCH] Add CI validation and project documentation CI validation (GitHub Actions + local scripts): - JSON syntax for all .json files - marketplace.json required fields (name, owner, plugins) - Plugin structure (plugin.json exists, skills have SKILL.md) - SKILL.md frontmatter (name, description required) - hooks.json format (object not array, valid event names) - Cross-platform: uses os.walk instead of find - Robust error handling for file/JSON operations Documentation: - CONTRIBUTING.md: Contribution workflow, plugin structure guide - AGENTS.md: Guidelines for LLMs working on this codebase - TESTING.md: Validation and testing instructions - CLAUDE.md: Quick reference pointing to other docs - README.md: Add CI badge and documentation section Also fixes: - Default RESEARCHER_DIR to $HOME/research - .git check to not skip .github directories Co-Authored-By: Claude Opus 4.5 --- .github/workflows/validate.yml | 32 +++++++ AGENTS.md | 81 ++++++++++++++++ CLAUDE.md | 26 +++++ CONTRIBUTING.md | 96 +++++++++++++++++++ README.md | 47 ++++----- TESTING.md | 94 ++++++++++++++++++ plugins/researcher/scripts/generate-pdfs.sh | 2 +- plugins/researcher/skills/researcher/SKILL.md | 2 +- scripts/validate.sh | 31 ++++++ scripts/validate_hooks.py | 57 +++++++++++ scripts/validate_json.py | 39 ++++++++ scripts/validate_marketplace.py | 74 ++++++++++++++ scripts/validate_skills.py | 50 ++++++++++ 13 files changed, 603 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/validate.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 CONTRIBUTING.md create mode 100644 TESTING.md create mode 100755 scripts/validate.sh create mode 100755 scripts/validate_hooks.py create mode 100755 scripts/validate_json.py create mode 100755 scripts/validate_marketplace.py create mode 100755 scripts/validate_skills.py diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..f5d76a4 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,32 @@ +name: Validate Plugin Structure + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate JSON syntax + run: python3 scripts/validate_json.py + + - name: Validate marketplace and plugin structure + run: python3 scripts/validate_marketplace.py + + - name: Validate SKILL.md frontmatter + run: python3 scripts/validate_skills.py + + - name: Validate hooks.json format + run: python3 scripts/validate_hooks.py + + - name: Summary + run: | + echo "" + echo "========================================" + echo " All validations passed!" + echo "========================================" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..23ede80 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,81 @@ +# Agent Guidelines + +Guidelines for LLMs working on this codebase. + +## Project Overview + +This is a Claude Code plugins marketplace. It contains: + +- **Marketplace definition**: `.claude-plugin/marketplace.json` +- **Plugins**: `plugins//` directories +- **Validation scripts**: `scripts/validate_*.py` +- **CI workflow**: `.github/workflows/validate.yml` + +## Key Constraints + +### Plugin Structure + +1. Each plugin needs `.claude-plugin/plugin.json` with: + - `name`, `description`, `version`, `author` + +2. Skills are auto-discovered from `skills/*/SKILL.md` + - Do NOT add explicit `skills` array to marketplace.json + - Each SKILL.md needs frontmatter with `name` and `description` + +3. hooks.json format is specific: + - Must be object keyed by event name, NOT an array + - Use `${CLAUDE_PLUGIN_ROOT}` for paths in commands + +### Validation + +Always run before committing: + +```bash +./scripts/validate.sh +``` + +Or run individual validators: + +```bash +python3 scripts/validate_json.py +python3 scripts/validate_marketplace.py +python3 scripts/validate_skills.py +python3 scripts/validate_hooks.py +``` + +### Common Mistakes to Avoid + +1. **hooks.json as array**: Must be `{"hooks": {"EventName": [...]}}` not `{"hooks": [...]}` +2. **Explicit skills array**: Don't add - causes path doubling +3. **Missing plugin.json**: Each plugin needs `.claude-plugin/plugin.json` +4. **echo for colors**: Use `printf` with `\e[0;32m` escapes in Makefiles + +## Testing Plugin Installation + +```bash +# Add marketplace +/plugin marketplace add jontsai/claude-plugins + +# Install plugin +/plugin install researcher@jontsai + +# Check installation +/plugin list +``` + +## File Locations + +| Purpose | Path | +|---------|------| +| Marketplace def | `.claude-plugin/marketplace.json` | +| Plugin metadata | `plugins//.claude-plugin/plugin.json` | +| Plugin hooks | `plugins//hooks/hooks.json` | +| Skills | `plugins//skills//SKILL.md` | +| Validation | `scripts/validate_*.py` | +| CI | `.github/workflows/validate.yml` | + +## Commit Guidelines + +- Run validation before committing +- Use clear, descriptive commit messages +- Squash fixup commits before merging diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..204423e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,26 @@ +# Claude Code Instructions + +This file provides guidance for Claude Code when working on this repository. + +## Quick Reference + +- **[AGENTS.md](./AGENTS.md)** - Technical guidelines for LLMs working on this codebase +- **[CONTRIBUTING.md](./CONTRIBUTING.md)** - Contribution workflow and standards +- **[TESTING.md](./TESTING.md)** - How to validate and test plugins + +## Before Making Changes + +1. Read [AGENTS.md](./AGENTS.md) for structural constraints +2. Run `./scripts/validate.sh` after changes +3. Follow patterns in existing plugins + +## Key Commands + +```bash +# Validate everything +./scripts/validate.sh + +# Test plugin install +/plugin marketplace add jontsai/claude-plugins +/plugin install researcher@jontsai +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2a69faf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# Contributing to @jontsai Claude Code Plugins + +Thank you for your interest in contributing! + +## Getting Started + +1. Fork the repository +2. Clone your fork locally +3. Create a feature branch: `git checkout -b my-feature` + +## Development Workflow + +### Before Committing + +Run the validation script to catch errors early: + +```bash +./scripts/validate.sh +``` + +This checks: +- JSON syntax in all `.json` files +- Marketplace and plugin structure +- SKILL.md frontmatter format +- hooks.json event names + +### Plugin Structure + +Each plugin must have: + +``` +plugins// +├── .claude-plugin/ +│ └── plugin.json # Required: name, description, version, author +├── hooks/ +│ └── hooks.json # Optional: lifecycle hooks +├── skills/ +│ └── / +│ └── SKILL.md # Required: frontmatter with name, description +├── scripts/ # Optional: helper scripts +├── LICENSE +├── Makefile # Optional: install/uninstall targets +└── README.md +``` + +### SKILL.md Frontmatter + +Every SKILL.md must have YAML frontmatter: + +```yaml +--- +name: skill-name +description: What the skill does +--- +``` + +### hooks.json Format + +Hooks must be an object keyed by event name: + +```json +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/my-hook.sh", + "timeout": 5000 + } + ] + } + ] + } +} +``` + +Valid events: `PreToolUse`, `PostToolUse`, `SessionStart`, `Stop`, `SubagentStop`, `SessionEnd`, `UserPromptSubmit`, `PermissionRequest`, `PreCompact`, `Notification` + +## Submitting Changes + +1. Ensure `./scripts/validate.sh` passes +2. Commit with clear, descriptive messages +3. Push to your fork +4. Open a pull request against `main` + +## Code Style + +- Keep scripts POSIX-compatible where possible +- Use `printf` instead of `echo` for colored output +- Prefer readability over cleverness + +## Questions? + +Open an issue on [GitHub](https://github.com/jontsai/claude-plugins/issues). diff --git a/README.md b/README.md index 426d5a2..ecb8f6b 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,53 @@ # @jontsai Claude Code Plugins -![Claude Code](https://img.shields.io/badge/Claude%20Code-Plugins-blueviolet?style=flat-square) -![License](https://img.shields.io/badge/License-MIT-green?style=flat-square) -![Plugins](https://img.shields.io/badge/Plugins-1-blue?style=flat-square) +[![Validate](https://github.com/jontsai/claude-plugins/actions/workflows/validate.yml/badge.svg)](https://github.com/jontsai/claude-plugins/actions/workflows/validate.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) Curated Claude Code plugins for research, productivity, and development workflows. -## Installation +## Quick Start ```bash -# Add this marketplace (one time) +# Add marketplace (one time) /plugin marketplace add jontsai/claude-plugins -# Install a plugin (example) +# Install a plugin /plugin install researcher@jontsai ``` -**Syntax:** -``` -/plugin install @jontsai -``` - ## Available Plugins | Plugin | Category | Description | |--------|----------|-------------| -| [`researcher`](./plugins/researcher/) | Productivity | Create distinctive HTML one-pagers with curated aesthetics, convert to PDF | +| [researcher](./plugins/researcher/) | Productivity | Create distinctive HTML/Markdown one-pagers with curated aesthetics, convert to PDF | ### researcher Create professional, visually striking research documents that avoid generic "AI slop" aesthetics. -**Features:** -- 5 curated aesthetic presets (Terminal, Editorial, Corporate, Industrial, Fresh) -- Cross-platform PDF generation via headless Chromium -- Print-optimized layouts with micro-interactions -- Makefile for easy setup +- **5 aesthetic presets:** Terminal, Editorial, Corporate, Industrial, Fresh +- **Dual output:** HTML (styled) or Markdown (plaintext) +- **Cross-platform:** macOS, Linux, Windows (WSL) +- **PDF generation:** via headless Chromium ```bash /plugin install researcher@jontsai ``` -## Categories +## Documentation + +- [CONTRIBUTING.md](./CONTRIBUTING.md) - How to contribute +- [TESTING.md](./TESTING.md) - Validation and testing +- [AGENTS.md](./AGENTS.md) - Guidelines for LLMs + +## Contributing -| Category | Description | -|----------|-------------| -| `productivity` | Tools for research, documentation, and workflow automation | -| `development` | Developer tools, code generation, and IDE enhancements | -| `learning` | Educational tools and interactive learning experiences | +Contributions welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) for details. ## License -MIT License - see individual plugin repositories for details. +[MIT](LICENSE) -## Author +--- -**Jonathan Tsai** · [GitHub](https://github.com/jontsai) · [Website](https://jontsai.org) +Created by [Jonathan Tsai](https://github.com/jontsai) diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..7fd4245 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,94 @@ +# Testing + +## Local Validation + +Run all validation checks: + +```bash +./scripts/validate.sh +``` + +Individual validators: + +```bash +# JSON syntax +python3 scripts/validate_json.py + +# Marketplace structure +python3 scripts/validate_marketplace.py + +# SKILL.md frontmatter +python3 scripts/validate_skills.py + +# hooks.json format +python3 scripts/validate_hooks.py +``` + +## Testing Plugin Installation + +### From Local Marketplace + +```bash +# Add local marketplace (use absolute path) +/plugin marketplace add /path/to/claude-plugins + +# Install plugin +/plugin install researcher@jontsai +``` + +### From GitHub + +```bash +# Add marketplace +/plugin marketplace add jontsai/claude-plugins + +# Install plugin +/plugin install researcher@jontsai + +# Verify +/plugin list +``` + +## Testing Individual Components + +### Skills + +Test that a skill loads correctly: + +```bash +# After installing plugin +/researcher +``` + +### Hooks + +SessionStart hooks run automatically when Claude Code starts a session. Check output for any hook messages. + +### Install Scripts + +Test the Makefile install: + +```bash +cd plugins/researcher +make install + +# Check output directory was created +ls -la ~/research +``` + +## CI + +The GitHub Actions workflow runs on: +- Push to `main` +- Pull requests to `main` + +It runs all four validation scripts and reports any errors. + +## Debugging + +### Common Issues + +1. **"expected record, received array"**: hooks.json format wrong +2. **"skills path not found"**: Don't use explicit skills array +3. **"duplicate hooks"**: Remove hooks field from plugin.json +4. **Colors not showing**: Use `printf` not `echo` in Makefile diff --git a/plugins/researcher/scripts/generate-pdfs.sh b/plugins/researcher/scripts/generate-pdfs.sh index 0e51abe..a640753 100644 --- a/plugins/researcher/scripts/generate-pdfs.sh +++ b/plugins/researcher/scripts/generate-pdfs.sh @@ -20,7 +20,7 @@ set -e # ============================================ # Use environment variable or default to ~/research -RESEARCHER_DIR="${RESEARCHER_DIR:-$HOME/researcher}" +RESEARCHER_DIR="${RESEARCHER_DIR:-$HOME/research}" HTML_DIR="$RESEARCHER_DIR/html" PDF_DIR="$RESEARCHER_DIR/pdf" diff --git a/plugins/researcher/skills/researcher/SKILL.md b/plugins/researcher/skills/researcher/SKILL.md index 8c93bf3..b17e8ad 100644 --- a/plugins/researcher/skills/researcher/SKILL.md +++ b/plugins/researcher/skills/researcher/SKILL.md @@ -17,7 +17,7 @@ Before using this skill, configure your output directory: ```bash # Add to your shell profile (~/.zshrc or ~/.bashrc) -export RESEARCHER_DIR="$HOME/researcher" +export RESEARCHER_DIR="$HOME/research" # Create the directory structure mkdir -p "$RESEARCHER_DIR/html" "$RESEARCHER_DIR/md" "$RESEARCHER_DIR/pdf" diff --git a/scripts/validate.sh b/scripts/validate.sh new file mode 100755 index 0000000..49ec6c3 --- /dev/null +++ b/scripts/validate.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Validate plugin structure locally +# Run this before pushing to catch errors early + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "$SCRIPT_DIR")" +cd "$ROOT_DIR" + +echo "" +echo "Validating Claude Code plugin structure..." +echo "" + +echo "1. Checking JSON syntax..." +python3 scripts/validate_json.py + +echo "2. Checking marketplace and plugin structure..." +python3 scripts/validate_marketplace.py + +echo "3. Checking SKILL.md frontmatter..." +python3 scripts/validate_skills.py + +echo "4. Checking hooks.json format..." +python3 scripts/validate_hooks.py + +echo "" +echo "========================================" +echo " All validations passed!" +echo "========================================" +echo "" diff --git a/scripts/validate_hooks.py b/scripts/validate_hooks.py new file mode 100755 index 0000000..168e733 --- /dev/null +++ b/scripts/validate_hooks.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Validate hooks.json format.""" + +import json +import os +import sys + +VALID_EVENTS = [ + "PreToolUse", + "PostToolUse", + "SessionStart", + "Stop", + "SubagentStop", + "SessionEnd", + "UserPromptSubmit", + "PermissionRequest", + "PreCompact", + "Notification", +] + + +def main(): + errors = [] + + for root, dirs, files in os.walk("."): + # Skip .git directory (but not .github) + if ".git" in root.split(os.sep): + continue + for f in files: + if f == "hooks.json": + path = os.path.join(root, f) + try: + with open(path, encoding="utf-8") as fp: + data = json.load(fp) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as e: + errors.append(f"{path}: Failed to load JSON: {e}") + continue + + hooks = data.get("hooks", {}) + if isinstance(hooks, list): + errors.append(f"{path}: 'hooks' must be an object, not array") + continue + + for event in hooks.keys(): + if event not in VALID_EVENTS: + errors.append(f"{path}: Unknown event '{event}'") + + if errors: + print("Errors found:") + for e in errors: + print(f" ✗ {e}") + sys.exit(1) + print("✓ All hooks.json files are valid") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_json.py b/scripts/validate_json.py new file mode 100755 index 0000000..e04a8d1 --- /dev/null +++ b/scripts/validate_json.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Validate all JSON files in the repository.""" + +import json +import os +import sys + + +def main(): + files = [] + for root, dirs, filenames in os.walk("."): + # Skip .git directory (but not .github) + if ".git" in root.split(os.sep): + continue + for f in filenames: + if f.endswith(".json"): + files.append(os.path.join(root, f)) + + errors = 0 + + for f in files: + try: + with open(f, encoding="utf-8") as fp: + json.load(fp) + except (OSError, UnicodeDecodeError) as e: + print(f" ✗ Cannot read: {f} - {e}") + errors += 1 + except json.JSONDecodeError as e: + print(f" ✗ Invalid JSON: {f} - {e}") + errors += 1 + + if errors: + print(f" ✗ {errors} JSON errors found") + sys.exit(1) + print("✓ All JSON files are valid") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_marketplace.py b/scripts/validate_marketplace.py new file mode 100755 index 0000000..27daa82 --- /dev/null +++ b/scripts/validate_marketplace.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +"""Validate marketplace.json structure.""" + +import json +import os +import sys + + +def main(): + try: + with open(".claude-plugin/marketplace.json", encoding="utf-8") as f: + data = json.load(f) + except FileNotFoundError: + print("Error: .claude-plugin/marketplace.json not found.") + sys.exit(1) + except PermissionError: + print("Error: Permission denied when reading .claude-plugin/marketplace.json.") + sys.exit(1) + except (OSError, UnicodeDecodeError) as e: + print(f"Error: Failed to read .claude-plugin/marketplace.json: {e}") + sys.exit(1) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON in .claude-plugin/marketplace.json: {e}") + sys.exit(1) + + errors = [] + + # Required top-level fields + for field in ["name", "owner", "plugins"]: + if field not in data: + errors.append(f"Missing required field: {field}") + + # Validate plugins + for plugin in data.get("plugins", []): + name = plugin.get("name", "") + if "name" not in plugin: + errors.append("Plugin missing required field: name") + if "source" not in plugin: + errors.append(f"Plugin '{name}' missing required field: source") + + # Check plugin.json exists + source = plugin.get("source", "") + plugin_json = os.path.join(source, ".claude-plugin", "plugin.json") + if not os.path.exists(plugin_json): + errors.append(f"Plugin '{name}': missing {plugin_json}") + + # Check skills have SKILL.md + skills_dir = os.path.join(source, "skills") + if os.path.exists(skills_dir): + try: + skill_entries = os.listdir(skills_dir) + except OSError as e: + errors.append(f"Plugin '{name}': cannot list skills directory: {e}") + continue + + for skill_name in skill_entries: + skill_path = os.path.join(skills_dir, skill_name) + if os.path.isdir(skill_path): + skill_md = os.path.join(skill_path, "SKILL.md") + if not os.path.exists(skill_md): + errors.append( + f"Plugin '{name}': skill '{skill_name}' missing SKILL.md" + ) + + if errors: + print("Errors found:") + for e in errors: + print(f" ✗ {e}") + sys.exit(1) + print("✓ marketplace.json and plugin structure valid") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_skills.py b/scripts/validate_skills.py new file mode 100755 index 0000000..ace0eb4 --- /dev/null +++ b/scripts/validate_skills.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Validate SKILL.md frontmatter.""" + +import os +import re +import sys + + +def main(): + errors = [] + + for root, dirs, files in os.walk("."): + # Skip .git directory (but not .github) + if ".git" in root.split(os.sep): + continue + for f in files: + if f == "SKILL.md": + path = os.path.join(root, f) + try: + with open(path, encoding="utf-8") as fp: + content = fp.read() + except (OSError, UnicodeDecodeError) as e: + errors.append(f"{path}: Unable to read file ({e})") + continue + + if not content.startswith("---"): + errors.append(f"{path}: Missing YAML frontmatter") + continue + + match = re.match(r"^---\s*\n(.*?)\n---\s*", content, re.DOTALL) + if not match: + errors.append(f"{path}: Invalid frontmatter format") + continue + + frontmatter = match.group(1) + if "name:" not in frontmatter: + errors.append(f"{path}: Missing 'name' in frontmatter") + if "description:" not in frontmatter: + errors.append(f"{path}: Missing 'description' in frontmatter") + + if errors: + print("Errors found:") + for e in errors: + print(f" ✗ {e}") + sys.exit(1) + print("✓ All SKILL.md files have valid frontmatter") + + +if __name__ == "__main__": + main()