mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-12 23:08:31 +00:00
feat: add cross-tool orientation, CI schema validation, and Claude Code surfaces
Adds the contributor- and AI-tool-facing infrastructure on top of the
modular docs from the previous commit. Lands AGENTS.md as the canonical
cross-tool orientation (read natively by Cursor, Copilot, Codex, Aider,
Cline, Continue, Cody, Devin, Replit Agent, and Claude Code), flips
CLAUDE.md to a 22-line pointer that uses Claude Code's @-import to
inline AGENTS.md, wires up plugin.json validation in CI, and adds the
Claude-specific automation surfaces under .claude/.
Cross-tool orientation:
AGENTS.md (178 lines) — single source of truth: architecture, running
the app, testing, git workflow, versioning, song formats, frontend
and backend conventions, plugin authoring index, first-hour
pitfalls, verification, house rules.
CLAUDE.md (22 lines) — Claude Code memory file. Uses @AGENTS.md
import (recursion depth 5) so the canonical content is inlined
without duplication. Lists .claude/ surfaces.
.github/copilot-instructions.md — Copilot custom instructions
format; points at AGENTS.md and docs/PLUGIN_AUTHORING.md.
.cursorrules — not added. Cursor reads AGENTS.md natively in 2026
and .cursorrules is legacy.
Contribution hygiene (.github/):
PULL_REQUEST_TEMPLATE.md — summary, linked issue, test plan, DCO
and conventional-commit reminders. No AI-disclosure section.
ISSUE_TEMPLATE/bug.yml — version, deployment, OS, plugins enabled,
repro, logs (linked to docs/diagnostics-bundle-spec.md for
redaction guidance).
ISSUE_TEMPLATE/feature.yml — problem, proposed, alternatives,
surface, plugin-author impact, license check.
ISSUE_TEMPLATE/config.yml — disables blank issues; redirects
plugin issues to plugin repos and security to the private
advisory flow.
CI:
.github/workflows/validate-plugins.yml — runs on changes to
plugins/*, schema/, CONTRIBUTING.md, the test file, or the
workflow itself. Installs jsonschema and pytest, validates every
plugins/*/plugin.json against schema/plugin.schema.json, and runs
the license-allowlist subset check.
tests/test_plugin_schema.py — 8 parametrized tests: schema is
well-formed, the 3 in-tree manifests validate, manifest id
matches its parent directory name, schema license enum is a
subset of CONTRIBUTING's curated allowlist.
requirements-test.txt — append jsonschema>=4.0.
.github/workflows/sync-version.yml — comment retargeted to
AGENTS.md "Versioning" section.
Claude Code surfaces (.claude/):
README.md — layout explanation. Spec-kit owns skills/speckit-*;
repo-specific skills sit alongside. Hooks off by default;
settings.json carries a commented opt-in example.
skills/plugin-scaffold/SKILL.md — generates a plugin skeleton for
type in {visualization, overlay, settings-only, routes-only}.
skills/plugin-validate/SKILL.md — local pre-push check: validates
plugin.json against schema, asserts declared files exist,
enforces license allowlist.
rules/plugin-author.md — globs scoped to plugins/**. Encodes the
contracts from docs/PLUGIN_AUTHORING.md so AI suggestions don't
drift from them (manifest required, context[\"log\"] over print,
load_sibling over bare imports, playSong await discipline,
settings.server_files conventions).
agents/slopsmith-reviewer.md — plugin-aware reviewer subagent;
invoke with @slopsmith-reviewer. 12-item checklist mirrors the
rule and the schema.
settings.json — empty hooks block plus a commented PostToolUse
example for opt-in plugin.json validation on save.
Inbound-ref updates (files we own):
README.md — \"AI Agent Guide\" points at AGENTS.md and notes
.claude/ and copilot-instructions are tool-specific.
CONTRIBUTING.md — \"Plugin System in CLAUDE.md\" -> docs/ and
schema/; \"Git Workflow\" -> AGENTS.md#git-workflow.
docs/sloppak-spec.md — plugin-system table cell -> docs/.
Out of scope (intentionally untouched):
plugins/highway_3d/README.md (gitlink — plugin owns its docs).
.specify/memory/constitution.md and other spec-kit artefacts
(spec-kit owns that surface; CLAUDE.md still resolves
transitively via the @-import).
Verification:
pytest -q # backend + schema tests pass
python -c \"import json,glob,jsonschema; s=json.load(open('schema/plugin.schema.json')); [jsonschema.validate(json.load(open(p)), s) for p in sorted(glob.glob('plugins/*/plugin.json'))]\"
Signed-off-by: Miguel_LZPF <mgcdreamer@gmail.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
# `.claude/` — Claude Code surfaces
|
||||
|
||||
This directory holds [Claude Code](https://claude.ai/code) artifacts: skills, rules, and subagents. **Other AI tools** (Cursor, Copilot, Codex, Aider) should read [`AGENTS.md`](../AGENTS.md) instead — `.claude/` is Claude-Code-specific.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
.claude/
|
||||
├── agents/ Subagents (invoked via @<name>)
|
||||
│ └── slopsmith-reviewer.md Plugin-aware code review
|
||||
├── rules/ Globs-scoped behaviour rules
|
||||
│ └── plugin-author.md Rules that fire when editing plugins/**
|
||||
├── skills/ Skills (description triggers auto-invocation)
|
||||
│ ├── plugin-scaffold/SKILL.md Generate a new plugin skeleton
|
||||
│ ├── plugin-validate/SKILL.md Validate a plugin.json against the schema
|
||||
│ └── speckit-*/ Spec-kit skills (auto-generated; don't edit)
|
||||
├── settings.json Repo defaults (no hooks enabled by default)
|
||||
└── README.md You are here
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Spec-kit owns `skills/speckit-*`.** Don't edit those manually — they're regenerated by the spec-kit installer (`.specify/`).
|
||||
- **Repo skills live alongside spec-kit skills** in `skills/`. New skills should have a kebab-case directory name and a `SKILL.md` with YAML frontmatter.
|
||||
- **Hooks are off by default.** Hooks in `.claude/settings.json` run in every contributor's Claude Code session — enabling one imposes work on everyone. The settings file ships with `hooks: {}` and a commented example so anyone who wants to opt in can copy it locally.
|
||||
- **Subagent invocation.** Use `@slopsmith-reviewer` to invoke a subagent explicitly. Plain auto-routing isn't currently configured.
|
||||
|
||||
## Adding a new skill
|
||||
|
||||
1. Create `.claude/skills/<kebab-name>/SKILL.md`.
|
||||
2. YAML frontmatter must include `name` (must match directory) and `description` (must read like a sentence — Claude Code matches the description against user intent for auto-invocation).
|
||||
3. Body: instructions Claude follows when the skill triggers. Keep it concrete and short — long skills bit-rot.
|
||||
|
||||
## Adding a new rule
|
||||
|
||||
1. Create `.claude/rules/<topic>.md`.
|
||||
2. Frontmatter may include `globs:` (array of glob patterns scoping when the rule applies). Without `globs`, the rule loads for every session.
|
||||
3. Body: terse, imperative guidance — "always do X", "never do Y", "if you see Z, …".
|
||||
|
||||
## Why so little here?
|
||||
|
||||
The maintainer's personal Claude Code rules (context-mode routing, RTK, `@git-worker`, etc.) deliberately do **not** live in this repo. Those are user-global concerns in `~/.claude/` — keeping them out of the repo keeps the shared surface lean and makes the repo equally useful to contributors using Cursor, Copilot, plain editors, etc.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: slopsmith-reviewer
|
||||
description: Plugin-aware code reviewer for Slopsmith. USE WHEN reviewing plugin changes, auditing a plugin against the manifest contract, checking that a plugin uses load_sibling / context["log"] / scoped shortcuts correctly, or verifying that a `plugin.json` matches the schema and the directory it lives in. Returns a structured pass/fail report with specific file:line citations.
|
||||
tools: Read, Grep, Glob, Bash
|
||||
model: sonnet
|
||||
---
|
||||
|
||||
# slopsmith-reviewer
|
||||
|
||||
Plugin-aware reviewer for Slopsmith. Use when reviewing a plugin's code or manifest. Does **not** duplicate the built-in `peer-review` skill — focus is narrow: the plugin contract surface defined in [`docs/PLUGIN_AUTHORING.md`](../../docs/PLUGIN_AUTHORING.md) and enforced by [`schema/plugin.schema.json`](../../schema/plugin.schema.json) and `tests/test_plugin_schema.py`.
|
||||
|
||||
## When to invoke
|
||||
|
||||
Use this agent when the user asks to:
|
||||
- "review this plugin"
|
||||
- "audit `plugins/<id>/`"
|
||||
- "check the manifest"
|
||||
- "verify the plugin uses load_sibling / logging correctly"
|
||||
- "lint plugin"
|
||||
|
||||
Do **not** invoke for general code review — use the built-in `peer-review` skill for that.
|
||||
|
||||
## Inputs
|
||||
|
||||
The user typically points at a directory: `plugins/<id>/`. If no directory is given, ask which plugin to review.
|
||||
|
||||
## Checklist (every review must run these)
|
||||
|
||||
Run each item; structure the output as `PASS` / `FAIL` / `N/A` with file:line citations.
|
||||
|
||||
1. **Manifest exists and validates.** `plugins/<id>/plugin.json` exists. Run:
|
||||
```bash
|
||||
python -c "import json,jsonschema; s=json.load(open('schema/plugin.schema.json')); jsonschema.validate(json.load(open('plugins/<id>/plugin.json')), s); print('OK')"
|
||||
```
|
||||
2. **Manifest `id` matches the directory name.** `tests/test_plugin_schema.py::test_in_tree_manifest_id_matches_directory` enforces this — but call it out in review.
|
||||
3. **Declared files exist.** For every path-bearing field in `plugin.json` (`script`, `routes`, `tour`, `settings.html`, `settings.server_files`, `diagnostics.server_files`), `test -f plugins/<id>/<path>` must succeed (or the path must be a directory if it ends with `/`).
|
||||
4. **License is on the curated allowlist** if present. Cross-check `plugin.json.license` against the SPDX list in [`CONTRIBUTING.md`](../../CONTRIBUTING.md) "Plugin licensing".
|
||||
5. **`type: "visualization"` ↔ `window.slopsmithViz_<id>` factory.** If `type == "visualization"`, grep `script` for the factory declaration.
|
||||
6. **Backend logging.** Grep `plugins/<id>/*.py` for `print(`, `traceback.print_exc(`, `logging.getLogger(`. Suggest `context["log"]` replacements.
|
||||
7. **Sibling imports.** If `routes.py` exists and grep finds bare `from <module> import` for any sibling Python file in the plugin dir, flag and recommend `context["load_sibling"]`.
|
||||
8. **Frontend IIFE.** If `script` exists, check the top of the file isn't running top-level statements that leak to global scope. Wrapping in `(function () { 'use strict'; ... })();` is the convention.
|
||||
9. **`playSong` wrapper discipline.** If the script reassigns `window.playSong`, confirm it calls the original and `await`s it.
|
||||
10. **Shortcut scope discipline.** If the script calls `window.registerShortcut`, confirm `scope` is set (not relying on the `'global'` default) and that an `unregisterShortcut` / `panel.clearShortcuts()` cleanup path exists when the plugin can be torn down.
|
||||
11. **`localStorage` prefix.** Grep for `localStorage.` usage; keys must start with `<plugin_id>`.
|
||||
12. **`settings.server_files` paths are safe.** Each entry must be a relpath — no leading `/`, no `..`, no backslashes. The schema enforces this but call it out.
|
||||
|
||||
## Output format
|
||||
|
||||
```
|
||||
plugin-review: <plugin_id>
|
||||
=========================
|
||||
1. manifest validates PASS
|
||||
2. id matches directory PASS
|
||||
3. declared files exist FAIL — settings.server_files lists "missing_db.sqlite" but plugins/<id>/missing_db.sqlite is absent
|
||||
...
|
||||
|
||||
Total: 10 PASS / 1 FAIL / 1 N/A
|
||||
Action items:
|
||||
- Remove the dangling settings.server_files entry, or create the file.
|
||||
- Replace `print(...)` calls at routes.py:42, routes.py:88 with `context["log"].info(...)`.
|
||||
```
|
||||
|
||||
If everything passes, say so explicitly — silent success is unhelpful in a review context.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- General code style / formatting — use the built-in `peer-review`.
|
||||
- Bug-hunting beyond the plugin contract.
|
||||
- Suggesting major refactors. Stay on contract compliance.
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: plugin-author
|
||||
description: Rules that apply when editing files under plugins/**. Enforces the plugin contracts documented in docs/PLUGIN_AUTHORING.md.
|
||||
globs:
|
||||
- "plugins/**"
|
||||
---
|
||||
|
||||
# Plugin authoring rules
|
||||
|
||||
These rules apply only when editing files under `plugins/**`. They encode the contracts described in [`docs/PLUGIN_AUTHORING.md`](../../docs/PLUGIN_AUTHORING.md) so AI suggestions don't drift from them.
|
||||
|
||||
## Manifest
|
||||
|
||||
- **`plugin.json` is required** and must validate against [`schema/plugin.schema.json`](../../schema/plugin.schema.json). Required fields: `id`, `name`. The `id` must match the parent directory name (the loader keys discovery by directory; drift breaks plugin lookup).
|
||||
- **License must come from the curated allowlist** if the plugin is intended for the curated list. See [`CONTRIBUTING.md`](../../CONTRIBUTING.md) "Plugin licensing".
|
||||
- **`type: "visualization"`** requires a `script` field exporting `window.slopsmithViz_<id>`. See [`docs/plugin-visualization-contracts.md`](../../docs/plugin-visualization-contracts.md).
|
||||
|
||||
## Backend (`routes.py`)
|
||||
|
||||
- **Use `context["log"]`, never `print()` or `traceback.print_exc()`.** The CI workflow blocks `print(` and `traceback.print_exc(` in `server.py` / `lib/`; plugin code should follow the same rule. The provided logger is a stdlib `logging.Logger` namespaced to `slopsmith.plugin.<id>` with correlation IDs, JSON mode, and rotation already wired. See [`docs/plugin-logging.md`](../../docs/plugin-logging.md).
|
||||
- **Multi-file plugins must use `context["load_sibling"]("<module>")`**, not bare `from <module> import X`. Two plugins shipping a same-named helper collide via `sys.modules`. See [`docs/plugin-sibling-imports.md`](../../docs/plugin-sibling-imports.md).
|
||||
- **`setup(app, context)` is the required entry.** Don't run side effects at import time.
|
||||
|
||||
## Frontend (`screen.js`)
|
||||
|
||||
- **Wrap in an IIFE** — `(function () { 'use strict'; ... })();`. Frontend scripts share global scope; leaking variables collides with other plugins.
|
||||
- **Hook `window.playSong` carefully** — always call the original, always `await` it. Wrappers run outermost-first; awaiting yields to the event loop and WebSocket messages can arrive before the outer wrapper finishes setup. Use `highway.getSongInfo()` as a fallback rather than relying solely on `_onReady`.
|
||||
- **Hook `window.showScreen`** — clean up your plugin's state when the user leaves the player screen.
|
||||
- **Use `window.slopsmith.emit` / `on`** for cross-plugin communication. Don't poll other plugins' globals.
|
||||
- **Register shortcuts with `window.registerShortcut({ key, scope, handler })`** and clean up with `window.unregisterShortcut(key, scope)` — pass the same scope you registered with (default `'global'` won't match `'player'` / `'plugin-*'`). For panel-scoped registries, prefer `panel.clearShortcuts()`. See [`docs/plugin-keyboard-shortcuts.md`](../../docs/plugin-keyboard-shortcuts.md).
|
||||
|
||||
## State and config
|
||||
|
||||
- **`localStorage` keys must be prefixed with the plugin id** to avoid collisions.
|
||||
- **`settings.server_files` declares config-dir paths the plugin wants included in the Settings export/import flow.** Relpaths only — no `..`, no abs paths, no backslashes. See [`docs/plugin-manifest.md`](../../docs/plugin-manifest.md).
|
||||
- **`diagnostics.server_files` / `diagnostics.callable`** declares what enters the Export Diagnostics bundle. Keep payloads under 100 KB and don't include secrets. See [`docs/plugin-diagnostics.md`](../../docs/plugin-diagnostics.md).
|
||||
|
||||
## Visualization specifics
|
||||
|
||||
When `plugin.json` declares `"type": "visualization"`:
|
||||
|
||||
- **Factory must be `window.slopsmithViz_<id>`** where `<id>` matches `plugin.json`.
|
||||
- **Factory must return a fresh object on each call** — splitscreen creates N instances.
|
||||
- **The renderer owns its `getContext()` call.** Declare `contextType: '2d'` or `'webgl2'` on the returned object so the highway can swap the canvas element when needed (`getContext` is one-shot per canvas).
|
||||
- **`draw(bundle)` receives difficulty-filtered arrays** — never read from `_filteredNotes` or other internals.
|
||||
|
||||
See [`docs/plugin-visualization-contracts.md`](../../docs/plugin-visualization-contracts.md) for the full lifecycle and the Overlay + Note-state-provider contracts.
|
||||
|
||||
## Testing
|
||||
|
||||
When changing plugin internals, add or update a test under `tests/` (Python) or `tests/js/` (Node) or `tests/browser/` (Playwright). See [`docs/testing-plugins.md`](../../docs/testing-plugins.md) for fixtures (`isolate_logging`, `reset_plugin_state`).
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||
"_comment": "Repo-default Claude Code settings for Slopsmith contributors. Hooks are intentionally empty by default — see commented example below.",
|
||||
|
||||
"hooks": {},
|
||||
|
||||
"_commented_example_hooks": {
|
||||
"_comment": "Copy the block below into the empty `hooks` object above if you want plugin.json edits to be auto-validated against schema/plugin.schema.json on save. Per-user opt-in; not enabled by default because hooks run in every Claude Code session that loads this repo.",
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"command": "if echo \"$CLAUDE_TOOL_INPUT_FILE_PATH\" | grep -qE '^plugins/[^/]+/plugin\\.json$'; then python -c \"import json,sys,jsonschema; s=json.load(open('schema/plugin.schema.json')); m=json.load(open('$CLAUDE_TOOL_INPUT_FILE_PATH')); jsonschema.validate(m,s); print('OK','$CLAUDE_TOOL_INPUT_FILE_PATH')\"; fi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: plugin-scaffold
|
||||
description: Scaffold a new Slopsmith plugin skeleton. USE WHEN the user asks to create a new plugin, scaffold a plugin, bootstrap a plugin, new visualization plugin, new overlay plugin, new settings-only plugin, plugin starter, plugin skeleton. Args needed - plugin slug (snake_case) and type (visualization / overlay / settings-only / routes-only). Generates plugins/<id>/ with plugin.json, screen.js, and optional routes.py / settings.html / Playwright test stub matching the requested type.
|
||||
---
|
||||
|
||||
# plugin-scaffold
|
||||
|
||||
Generates a minimum-viable Slopsmith plugin skeleton matching a requested shape. The output validates against [`schema/plugin.schema.json`](../../../schema/plugin.schema.json).
|
||||
|
||||
## When to invoke
|
||||
|
||||
The user says one of:
|
||||
- "scaffold a new plugin called X"
|
||||
- "create a visualization plugin"
|
||||
- "new overlay plugin"
|
||||
- "plugin starter for settings"
|
||||
- "bootstrap a routes-only plugin"
|
||||
|
||||
If the plugin slug or type is missing, ask once.
|
||||
|
||||
## Inputs
|
||||
|
||||
| Arg | Required | Values | Notes |
|
||||
|---|---|---|---|
|
||||
| `id` | yes | snake_case | Becomes plugin's `id` field and directory name |
|
||||
| `name` | optional | string | Defaults to title-case of `id` |
|
||||
| `type` | yes | `visualization` / `overlay` / `settings-only` / `routes-only` | Determines which files get scaffolded |
|
||||
|
||||
## What to generate
|
||||
|
||||
**Common to all types:**
|
||||
|
||||
- `plugins/<id>/plugin.json` — minimum schema-valid manifest. Set `id`, `name`, `version: "0.1.0"`, and `license: "AGPL-3.0-only"` by default (ask if a different license is desired).
|
||||
|
||||
**`type=visualization`** — adds:
|
||||
- `"type": "visualization"` and `"script": "screen.js"` to manifest
|
||||
- `screen.js` exporting `window.slopsmithViz_<id> = function () { return { contextType: '2d', init(canvas, bundle) { this.ctx = canvas.getContext('2d'); }, draw(bundle) { /* TODO */ }, destroy() {} }; };` plus a static `matchesArrangement` example commented out
|
||||
- `tests/browser/<id>.spec.ts` — Playwright stub that loads the app and asserts the plugin's factory is registered
|
||||
|
||||
**`type=overlay`** — adds:
|
||||
- `"script": "screen.js"` to manifest (no `type` declared — overlays don't use the picker)
|
||||
- `screen.js` scaffolding a navbar toggle, an own-canvas + own-rAF loop reading `highway.getNotes()` / `getChords()` / `getTime()`, and respecting `highway.isDefaultRenderer()` if using `highway.project` / `fretX`
|
||||
- `tests/browser/<id>.spec.ts` — toggle on / off test
|
||||
|
||||
**`type=settings-only`** — adds:
|
||||
- `"settings": { "html": "settings.html" }` to manifest
|
||||
- `settings.html` — empty form skeleton with explanatory comments
|
||||
- `screen.js` reading/writing `localStorage` keys prefixed with `<id>_`
|
||||
|
||||
**`type=routes-only`** — adds:
|
||||
- `"routes": "routes.py"` to manifest
|
||||
- `routes.py` with `def setup(app, context):` that registers one example route and uses `context["log"].info("plugin ready")` (never `print()`)
|
||||
- `tests/test_<id>_routes.py` — FastAPI TestClient stub
|
||||
|
||||
## After scaffolding
|
||||
|
||||
Run validation locally:
|
||||
|
||||
```bash
|
||||
python -c "import json,jsonschema; s=json.load(open('schema/plugin.schema.json')); jsonschema.validate(json.load(open('plugins/<id>/plugin.json')), s); print('OK')"
|
||||
```
|
||||
|
||||
Then point the user at [`docs/PLUGIN_AUTHORING.md`](../../../docs/PLUGIN_AUTHORING.md) and the relevant contract doc for the type they chose.
|
||||
|
||||
## Don'ts
|
||||
|
||||
- Don't add fields to the manifest that aren't in the schema. If the user wants something custom, ask whether it should become a real field — that's a `schema/plugin.schema.json` change, not a plugin-local convention.
|
||||
- Don't scaffold a `requirements.txt` without confirming the deps. The plugin loader installs them on first load; an accidental dep slows everyone's startup.
|
||||
- Don't pre-fill `localStorage` keys without a prefix. Collisions across plugins are real.
|
||||
- Don't generate hidden side effects at import time (`screen.js` top level or `routes.py` top level). Keep all wiring inside the IIFE / `setup()`.
|
||||
|
||||
## Verification
|
||||
|
||||
The scaffolded plugin should pass:
|
||||
|
||||
```bash
|
||||
pytest tests/test_plugin_schema.py::test_in_tree_manifest_validates -v
|
||||
pytest tests/test_plugin_schema.py::test_in_tree_manifest_id_matches_directory -v
|
||||
```
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
name: plugin-validate
|
||||
description: Validate a Slopsmith plugin's plugin.json against the schema and run structural checks. USE WHEN the user asks to validate a plugin, check the manifest, plugin.json errors, lint plugin, verify plugin structure, plugin license check, or audit plugin contract. Runs JSON Schema validation, file-existence checks for declared script/routes/settings.html/tour paths, and license-allowlist check.
|
||||
---
|
||||
|
||||
# plugin-validate
|
||||
|
||||
Runs the same checks `.github/workflows/validate-plugins.yml` + `tests/test_plugin_schema.py` run in CI, but **locally and instantly** — useful for catching errors before push.
|
||||
|
||||
## When to invoke
|
||||
|
||||
The user says one of:
|
||||
- "validate plugins/<id>"
|
||||
- "check the manifest for X"
|
||||
- "lint this plugin"
|
||||
- "verify my plugin.json"
|
||||
- "plugin license check"
|
||||
|
||||
Works on either a specific plugin (e.g. `plugins/highway_3d/`) or all in-tree plugins if none is specified.
|
||||
|
||||
## What to run
|
||||
|
||||
```bash
|
||||
# All in-tree plugins (default)
|
||||
python <<'PY'
|
||||
import json, glob, sys, jsonschema, pathlib
|
||||
schema = json.load(open('schema/plugin.schema.json'))
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
ok = True
|
||||
for path in sorted(glob.glob('plugins/*/plugin.json')):
|
||||
plugin_dir = pathlib.Path(path).parent
|
||||
plugin_id = plugin_dir.name
|
||||
m = json.load(open(path))
|
||||
# 1. Schema
|
||||
try:
|
||||
jsonschema.validate(m, schema)
|
||||
except jsonschema.ValidationError as e:
|
||||
print(f"FAIL {path}: {e.message} (at {list(e.absolute_path)})")
|
||||
ok = False
|
||||
continue
|
||||
# 2. id == directory name
|
||||
if m['id'] != plugin_id:
|
||||
print(f"FAIL {path}: id={m['id']!r} but directory is {plugin_id!r}")
|
||||
ok = False
|
||||
# 3. Declared files exist
|
||||
for field in ('script', 'routes', 'tour'):
|
||||
if field in m and not (plugin_dir / m[field]).exists():
|
||||
print(f"FAIL {path}: {field}={m[field]!r} but file missing")
|
||||
ok = False
|
||||
if 'settings' in m and 'html' in m['settings']:
|
||||
h = m['settings']['html']
|
||||
if not (plugin_dir / h).exists():
|
||||
print(f"FAIL {path}: settings.html={h!r} but file missing")
|
||||
ok = False
|
||||
for field in ('settings', 'diagnostics'):
|
||||
if field in m and 'server_files' in m[field]:
|
||||
for relpath in m[field]['server_files']:
|
||||
# server_files paths live under context["config_dir"], NOT the
|
||||
# plugin dir, so we don't existence-check them here. We only
|
||||
# confirm the path *looks* safe (already enforced by schema).
|
||||
if '..' in relpath or relpath.startswith('/') or '\\' in relpath:
|
||||
print(f"FAIL {path}: {field}.server_files contains unsafe path {relpath!r}")
|
||||
ok = False
|
||||
print(f"OK {path}")
|
||||
sys.exit(0 if ok else 1)
|
||||
PY
|
||||
```
|
||||
|
||||
## Targeted invocation
|
||||
|
||||
If the user names a specific plugin, swap the glob for `plugins/<id>/plugin.json` and report on just that one.
|
||||
|
||||
## License-allowlist check
|
||||
|
||||
If the user specifically asks for a license check (or the plugin declares `license` in `plugin.json`), additionally run:
|
||||
|
||||
```bash
|
||||
pytest tests/test_plugin_schema.py::test_schema_license_enum_subset_of_contributing_allowlist -v --noconftest
|
||||
```
|
||||
|
||||
(or skip `--noconftest` if `structlog` is installed locally).
|
||||
|
||||
## Output
|
||||
|
||||
Use the format from the script: `OK <path>` per validated manifest, `FAIL <path>: <reason>` per failure. Add a one-line summary:
|
||||
|
||||
```
|
||||
Result: 3/3 plugins valid (OK app_tour_library, app_tour_settings, highway_3d)
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
Result: 2/3 plugins valid; 1 FAIL (see above)
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [`schema/plugin.schema.json`](../../../schema/plugin.schema.json)
|
||||
- [`tests/test_plugin_schema.py`](../../../tests/test_plugin_schema.py)
|
||||
- [`.github/workflows/validate-plugins.yml`](../../../.github/workflows/validate-plugins.yml)
|
||||
- [`docs/plugin-manifest.md`](../../../docs/plugin-manifest.md)
|
||||
Reference in New Issue
Block a user