| name | new-plugin |
| description | Build a complete FiestaBoard plugin end-to-end — code, manifest, tests, docs, CI, release automation, and registry PR. Use this skill whenever the user wants to create, scaffold, build, or write a new FiestaBoard plugin or data-source integration (weather, transit, stocks, calendars, jokes, art, home-automation, any feed), turn an API or data source into a board display, or publish a plugin to the registry. Trigger eagerly on phrases like "make a plugin", "new plugin", "scaffold a plugin", "build a FiestaBoard integration", "I want a plugin that shows X on my board", "add a plugin for <service>", "publish my plugin", or any request that ends in a board displaying live data. Use this even when the user does not say the word "plugin" — if the intent is "get <some data> onto a Vestaboard/FiestaBoard", that is this skill. Also handles bundled in-repo plugins (plugins/<id>/) as a secondary mode. |
new-plugin
What this skill does
Scaffolds and finishes a complete FiestaBoard plugin in one pass: the PluginBase
implementation, the manifest.json, the test suite, the docs, the GitHub Actions CI
and release automation, and — when the user wants to publish — the GitHub repo and
the registry PR. The goal is that a contributor with an idea ("I want tide times on my
board") ends with a green, publishable fiestaboard-plugin--<slug> repo, not a pile of
TODOs.
The hard, error-prone, mechanical parts (naming invariants, the symlink-based CI import
contract, the verbatim demo-validation test, the release workflow) are generated by a
script so they're always correct. Your attention goes where judgment is actually needed:
modeling the data source, choosing good template variables, and writing honest docs.
Primary output: a standalone published repo fiestaboard-plugin--<slug> (root
layout, symlink CI, registry PR). Secondary mode: a bundled in-repo plugin under
plugins/<id>/, like countdown/date_time/random.
When NOT to use this skill
- Modifying an existing plugin's behavior → just edit it; you don't need scaffolding.
- A core/platform feature in
src/ → that's new-feature, not a plugin.
- "Register a plugin that already exists as a repo" -> you only need Step 7
(the registry PR). Skip scaffolding.
The shape of a plugin
A standalone plugin is a self-contained GitHub repo. It is not a pip package — there
is no pyproject.toml. Its CI checks out the FiestaBoard core repo alongside it and runs
the tests against it via PYTHONPATH.
fiestaboard-plugin--<slug>/
├── __init__.py # PluginBase subclass; ends with `Plugin = <Class>`
├── manifest.json # id, name, version, settings, variables, demo, screenshots
├── README.md LICENSE .gitignore
├── run_tests.sh # local test helper (mirrors CI)
├── .github/workflows/
│ ├── ci.yml # test on push/PR, --cov-fail-under=70
│ └── release.yml # tag + GitHub Release when manifest version bumps
├── docs/
│ ├── board-display.png # hero image (scaffold emits a PLACEHOLDER — replace it)
│ └── SETUP.md
└── tests/
├── __init__.py conftest.py
├── test_plugin.py # plugin_id, fetch_data, manifest metadata
└── test_demo_pages.py # verbatim — validates demo templates vs declared vars
Naming invariants — these four must always agree
From one kebab-case slug the rest is mechanical (the scaffold script does this for you):
| Form | Example | Rule |
|---|
| slug | tide-times | what the user picks; lowercase kebab |
| id | tide_times | slug with -→_. This is the manifest id, the import name, and what plugin_id returns. The loader asserts all three match. |
| class | TideTimesPlugin | PascalCase of id + Plugin |
| repo | fiestaboard-plugin--tide-times | fiestaboard-plugin-- + slug (note the double hyphen) |
The display name (Tide Times) is free-form Title Case and not mechanically derived.
The workflow
Default to the standalone published-repo path. Detect bundled mode only if the user
explicitly wants an in-repo plugin shipped inside FiestaBoard itself.
Step 1 — Interview
You need a small, concrete spec before scaffolding. Ask only what you can't infer; suggest
defaults. Use one AskUserQuestion batch in Claude Code.
- What should the board show, and where does the data come from? This is the heart of
it. Get the data source (an HTTP API? a local computation? an ICS feed?), whether it
needs an API key, and what the user wants on the board. Their words seed the docs.
- slug + display name — propose them from the idea (e.g. "tide times" → slug
tide-times, name Tide Times). Confirm.
- category — one of
art, data, transit, weather, entertainment, utility,
home. Never invent a category. Pick the closest; default utility.
- icon — a Lucide icon name (e.g.
waves, banknote, plane).
Default puzzle.
- author — default
FiestaBoard Team unless the user gives a name/email (author
attribution is the one place real contact info is allowed — see CLAUDE.md).
- type —
http (fetches from an API; tests mock the network) or simple (computes
locally, no network). Most data plugins are http. Pick based on the data source.
Richer shapes — art (color-tile grids), trigger (push a page on an event), and
webhook (receive inbound payloads) — start from http/simple and are hand-extended.
Read references/plugin-types.md when the idea is one of those.
Auth matters more than it looks — ask about it now. FiestaBoard usually runs as a LAN
appliance with no public domain, so redirect-based OAuth typically can't complete (no
reachable redirect_uri). Before committing to an integration, confirm there's a viable
path: an API key / token the user pastes in (a password-widget setting + env_vars entry;
never hardcode it), an OAuth device-authorization flow, or a long-lived token. If the
service only offers redirect OAuth, surface that now — it's a poor fit. The auth section of
references/design-guidance.md covers the patterns.
Step 2 — Scaffold
Run the generator. It writes a green-by-default skeleton — its tests pass immediately,
so you have a known-good baseline to evolve. Standalone repos go next to FiestaBoard (the
sibling layout the registry CI expects).
python3 .claude/skills/new-plugin/scripts/scaffold_plugin.py \
--slug tide-times --name "Tide Times" \
--description "Display upcoming high and low tides for a coastal station." \
--author "FiestaBoard Team" --category weather --icon waves --type http \
--output-dir ..
The generated manifest.json repository URL needs the right GitHub owner. The script
defaults it to your authenticated gh login (run gh api user -q .login to see it) — it
does not assume the Fiestaboard org, which only maintainers can publish under. Pass
--owner <username-or-org> to override (e.g. a maintainer using --owner Fiestaboard, or to
publish under an org you have create-access to). If the script can't detect a login it writes
a YOUR-GITHUB-USERNAME placeholder and warns — fix it before publishing.
Use python3 (the host may not have a bare python). For a bundled plugin add --bundled
and --output-dir . (creates plugins/<id>/ in the current repo). Run
scaffold_plugin.py --help for all flags.
cd into the new repo and git init it (standalone only). Just git init here — no commit
yet; the first commit happens at publish time in Step 6.
Step 3 — Verify the green skeleton BEFORE editing
Confirm the harness works before you change anything — that way, if a test breaks later,
you know it was your edit. Run the tests inside the dev container (it already has the core
- all deps; this is the faithful mirror of the plugin's CI, and keeps Python deps in Docker
per CLAUDE.md):
.claude/skills/new-plugin/scripts/run_tests_in_container.sh ../fiestaboard-plugin--tide-times
You should see all tests pass with coverage ≥ 70%. The helper finds the dev container by
name (robust to compose drift) and installs pytest into it if a rebuild dropped it, so a
bare pytest: not found shouldn't stop you. (It does need the container running — /start
if it isn't.) The plugin's own run_tests.sh is the host-venv fallback for end users;
prefer the container path while building.
Step 4 — Implement (and keep the tests green the whole time)
Now replace the example logic with the real thing. The scaffold left clearly-marked TODOs.
Work in small steps, re-running Step 3 after each so you never drift far from green.
Read references/pluginbase-contract.md (the methods, PluginResult, the board-output
format) and references/manifest-reference.md (every manifest field) before writing real
logic — and references/design-guidance.md, which is what turns a correct plugin into a
good one: how to design variables people actually want on a board (display-ready values,
units, color tiles, graceful unavailable states) and a config a non-developer can complete
(minimal required fields, clear titles, actionable validation). The essentials:
fetch_data(self) -> PluginResult is the one required method. Do the real fetch/
compute, return PluginResult(available=True, data={...}). It must never raise —
wrap the body in try/except and return PluginResult(available=False, error=str(e))
on failure. Downstream rendering depends on this contract.
data keys become template variables {{<id>.<key>}}. Every key you return should
be declared in the manifest's variables.simple, and every declared variable should
appear in data. The test suite checks this both ways — that's not red tape, it's what
keeps the variable picker in the UI honest.
- The manifest
demo template may only reference declared variables (test_demo_pages.py
enforces it). When you change variables, update the demo template too.
get_formatted_display() (optional) returns the fallback rendering: a list of exactly
6 strings, each ≤ 22 chars (flagship board geometry). Not a string — a list. There is
a regression test guarding against the string bug; keep it a list.
- The board has a limited character set. Any text that lands on the board (variable
values,
formatted_lines) must use only supported characters — uppercase letters, digits,
space, and a handful of punctuation. No unicode, no emoji, and notably no backslash
(so an emoticon like \m/ is unrenderable). The authoritative map is src/board_chars.py
/ src/text_to_board.py in the core repo. For any jokes/quotes/facts/emoticon plugin, add
a test asserting every output string is board-safe — it's the failure mode reviewers hit.
- Keep
plugin_id, the manifest id, and the directory name identical.
- Keep the module-level
Plugin = <Class> export — the loader looks for it.
Then make the tests real: for http plugins, mock requests.get with a realistic
payload from the actual API and assert on the parsed values (model the dad-jokes/currency
tests). Aim comfortably above the 70% gate.
Finally, write the docs. README.md and docs/SETUP.md follow a fixed section order
(already stubbed). Fill the TODOs with real specifics — what it shows, the variables table,
a working example template, the settings, and honest troubleshooting. Use only generic
example values (example@example.com, your-api-key-here, public landmark coordinates) —
never real personal data, addresses, or keys (CLAUDE.md).
Step 5 — Re-verify, then stop and show the user
Run Step 3 once more and confirm green. Show the user a concise summary: the data source,
the variables, an example board template, and the file tree. The docs/board-display.png
is still a placeholder — flag that it must be replaced with a real board render before the
registry PR (offer to help capture one from the running app's demo page, or leave it for the
user). Confirm they're happy before any outward/network action.
Step 6 — Publish (gated)
Only after the user confirms. Each network action is outward-facing — confirm before each.
- Commit on
main in the new repo with a conventional message
(feat: initial <name> plugin).
- Pick the repo owner — do NOT assume the
Fiestaboard org. The repo is created under
whatever account/org the user actually has create-access to, which for most contributors
is their own GitHub account. Determine the options and confirm with the user before
creating:
gh api user -q .login
gh api user/orgs -q '.[].login'
Use Fiestaboard only if it shows up in that org list and the user wants it there;
otherwise default to their own account (or another org they can create in). Then create
and push under the chosen owner:
OWNER=<the-user's-login-or-an-org-they-can-create-in>
gh repo create "$OWNER/fiestaboard-plugin--tide-times" --public --source=. --push
Whatever owner you use, the registry entry's repository URL (Step 7) must point at
that exact namespace — https://github.com/$OWNER/fiestaboard-plugin--tide-times.
- Watch CI go green on the pushed branch (
gh run watch / gh run list). CI is the
authoritative gate — the container run in Step 3 is the local mirror, but the registry
checklist requires CI passing on the default branch.
Bundled plugins skip this entire step — they live inside the FiestaBoard repo and ride its
PR/CI. For those, open a normal feature-branch PR per CLAUDE.md.
Step 7 — Register (gated)
A plugin is invisible until it's in the registry. This is a PR against the main
FiestaBoard repo. Read references/publishing-and-registry.md for the full schema and
validation rules. In short:
- Add one 8-field entry to the
plugins array in plugin-registry.json, kept
alphabetical by id:
{
"id": "tide_times",
"name": "Tide Times",
"description": "Display upcoming high and low tides for a coastal station.",
"repository": "https://github.com/Fiestaboard/fiestaboard-plugin--tide-times",
"author": "FiestaBoard Team",
"fiestaboard_version": ">=4.2.0",
"icon": "waves",
"category": "weather"
}
- Add a row to the "All Available Plugins" table in the main
README.md (the human
mirror), alphabetically.
- Validate locally before pushing:
python scripts/validate_plugins.py --registry --verbose (run in the FiestaBoard repo /
dev container). It checks naming, id↔repo agreement, semver constraint, URL reachability,
and uniqueness — the same gate CI runs.
- Open the PR against
main with gh pr create. Per CLAUDE.md, never commit registry
changes directly to main — always a branch + PR.
Step 8 — Tell the user what's next
- The repo URL and CI status.
- The registry PR URL.
- That
docs/board-display.png still needs a real render (if not yet done).
- How a release happens: bump
version in manifest.json, merge to main, and
release.yml tags v<version> + cuts a GitHub Release automatically.
Hard rules
fetch_data must never raise. Catch everything; return available=False, error=....
- Keep the four names in lockstep (slug→id,
plugin_id, manifest id, directory). The
loader asserts they match; CI validates id↔repo.
formatted_lines is a list of 6 strings ≤22 chars, never a string.
- Keep manifest variables, returned
data keys, and the demo template in sync. The
tests enforce it; drift means a broken variable picker.
- Category is one of the seven — never invent one.
- Never use real personal data or API keys anywhere (code, tests, docs). Author
attribution is the only allowed real contact info.
- Confirm before outward actions —
gh repo create, git push, and opening the
registry PR. Approval for one isn't approval for the next.
- Never commit registry changes directly to
main. Branch + PR.
- Replace the placeholder
board-display.png before registering — published plugins
require a real hero image.
References
references/pluginbase-contract.md — every method, PluginResult, the board-output format.
references/manifest-reference.md — full manifest.json field reference.
references/design-guidance.md — auth on a domain-less appliance, variable design, config UX.
references/plugin-types.md — simple / http / art / trigger / webhook variants.
references/publishing-and-registry.md — repo creation, the registry entry schema, validation.
- Real examples next to this repo:
../fiestaboard-plugin--dad-jokes (simple http, root
layout like the scaffold), ../fiestaboard-plugin--weather (http + API key via password
widget + env_var), ../fiestaboard-plugin--sun-art (art),
../fiestaboard-plugin--calendar-sub (triggers). And plugins/_template/ for the canonical
skeleton. Note ../fiestaboard-plugin--currency uses the alternative nested layout
(plugins/<id>/ + root shim) — fine to read for fetch logic, but don't copy its structure
for a root-layout scaffold.