Plugins
Add spinners, borders, glyphs, colour tokens and components as one plain object, validated at register() and checked before it ships with flagstaff check.
A flagstaff plugin is one plain object, validated against
schema.json —
the same file that ships in the package as flagstaff/schema.json — and registered once. The
built-in spinners, borders and glyphs are a plugin of exactly this shape, registered through
the same door, so the built-ins cannot use anything a plugin cannot.
The shape
export default {
name: 'nyan',
spinners: { nyan: { frames: ['≋', '≈', '~'], interval: 80, static: '~nyan~' } },
};| key | what it contributes |
|---|---|
name | the plugin's name, in flagstaff check and the gallery |
spinners | { frames, interval, static } per style |
borders | the eight characters of a box border, in cli-boxes' shape |
glyphs | ok, fail, warn, info, running — change one and every built-in that draws it changes |
tokens | a roundel colour theme, as #rrggbb values |
components | { static, frame?, interval?, sample? } per component |
contract | the plugin contract version, 1; a newer one is refused with the upgrade named |
Keys another package in the family reads may sit in the same object; flagstaff keeps only its own.
Registering
import { hoist } from 'flagstaff/loop';
import { register } from 'flagstaff/plugin';
import { spinner } from 'flagstaff/spinner';
import nyan from './nyan.mjs';
import { json, rt } from './rt.mjs';
register(nyan);
hoist(spinner('nyan'), rt, { text: 'working' }, { json }).lower({ text: 'done', status: 'ok' });~nyan~ working
✔ doneA later registration of the same name replaces the earlier one, which is how you override a
built-in. register() is the only way in: registered() hands back a copy, so setting or
clearing what it returns changes nothing, and what was registered is frozen, so editing your
object afterwards does not change the registry either
(plugin.test.ts).
Refusals
A plugin the schema refuses throws a PluginError with a code and a fix. The one mistake
made on purpose — an animation with no text form — has its own:
import { register } from 'flagstaff/plugin';
try {
register({ name: 'bad', spinners: { bad: { frames: ['a', 'b'], interval: 80 } } });
} catch (error) {
console.log(`${error.code}: ${error.message}`);
console.log(`fix: ${error.fix}`);
}E_NO_STATIC_PROJECTION: bad has no static projection
fix: give it a `static`: the text a pipe, an agent or a screen reader gets instead of the animationEvery refusal any flagstaff surface prints is one of eight codes — E_PLUGIN_SCHEMA,
E_NO_STATIC_PROJECTION, E_PLUGIN_CONTRACT, E_UNKNOWN_SPINNER, E_UNKNOWN_BORDER,
E_NO_CONTRIBUTION, E_COMPONENT_THREW, E_UNKNOWN_KIND — and a fuzz test hands register()
arbitrary input to hold that it either registers or refuses with a code and a fix, never an
unhandled throw
(plugin-fuzz.test.ts).
flagstaff check
Before a plugin ships, render it. flagstaff check loads the file, validates it, and prints
every contribution in all five modes side by side, escapes made visible:
nyan — 1 spinner, 0 borders, 0 components, 0 glyphs, 0 tokens
spinner nyan
tty ␛[?25l≋ working␛[1G␛[0J≈ working␛[1G␛[0J~ working␛[1G␛[0J≋ working␛[1G␛[0J✔ done⏎ ␛[?25h
pipe ~nyan~ working⏎ ✔ done⏎
ci ~nyan~ working⏎ ✔ done⏎
json {"event":"spinner","state":{"text":"working"}}⏎ {"event":"spinner","state":{"text":"done","status":"ok"}}⏎
accessible ~nyan~ working⏎ ✔ done⏎
nyan: okIt opens with a census, so a misspelled key shows as 0 spinners, and closes with the verdict,
so ok is never printed before the rendering that justifies it. A refusal exits 1 with the code
and the fix; a plugin that validates but contributes nothing flagstaff can render is
E_NO_CONTRIBUTION, and a component whose static throws is E_COMPONENT_THREW, naming the
modes it broke in.
Components and sample
A component contributed by a plugin may declare sample: { running, done }, the two states
flagstaff check and the docs gallery show it with. The loop never reads it. Without one, both
assume { phase: 'running' } and { phase: 'done' } and say so in the output, rather than
rendering an invented state as if it were yours.
Bringing a corpus
cli-spinners' styles and cli-boxes' borders are not bundled. flagstaff/import turns the copy
you already depend on into an ordinary plugin, through the same register() and schema:
import cliSpinners from 'cli-spinners';
import { fromCliSpinners } from 'flagstaff/import';
import { register } from 'flagstaff/plugin';
import { spinner } from 'flagstaff/spinner';
register(fromCliSpinners(cliSpinners));
spinner('moon');cli-spinners has no text form for its styles, so fromCliSpinners gives each one …, or
whatever your staticFor(name, spinner) returns. fromCliBoxes(cliBoxes) does the same for
borders; their shape is already flagstaff's.
NO_COLOR and accessibility
How flagstaff decides colour and redraws — NO_COLOR, FORCE_COLOR, --color, CLI_ACCESSIBLE — and what a screen-reader user gets.
Why flagstaff
flagstaff against ora, log-update, boxen and cli-table3, one capability per row, every cell linked to the test, grade or source that proves it.