# Plugin protocol
primate's built-in generators (Rust, TypeScript, Python) are part of
the binary, but they implement the same protocol every plugin does:
**JSON request on stdin, JSON response on stdout, errors on stderr.**
You can plug in any executable on `PATH` (or a script path) as a
generator by referencing it from `primate.toml`.
## Wire format
primate spawns the plugin and writes a single JSON request to its
stdin, then closes stdin. The plugin reads stdin, generates whatever
it generates, and writes a single JSON response to stdout.
### Request
```json
{
"version": 1,
"outputPath": "scripts/generated/constants.lua",
"options": { "naming": "camelCase" },
"modules": [
{
"namespace": "limits",
"sourceFile": "constants/limits.prim",
"doc": null,
"constants": [
{
"name": "TIMEOUT",
"doc": "How long the gateway waits before bailing.",
"type": { "kind": "duration" },
"value": { "duration": { "nanoseconds": 30000000000 } },
"source": { "file": "constants/limits.prim", "line": 4, "column": 10 }
}
]
}
],
"enums": [...],
"aliases": [...]
}
```
Top-level fields:
- `version` — protocol version. Currently `1`.
- `outputPath` — the `output` value from the relevant `[generators.*]`
section of `primate.toml`, relative to project root.
- `options` — every other key from that `[generators.*]` section,
passed through verbatim as a JSON object.
- `modules` — one per namespace; carries that namespace's constants.
- `enums`, `aliases` — top-level enum and type-alias definitions.
### Response
```json
{
"files": [
{
"path": "scripts/generated/constants.lua",
"content": "-- generated by primate\n...",
"mappings": [
{ "symbol": "limits::TIMEOUT", "line": 7, "column": 1 }
]
}
],
"errors": []
}
```
- `files` — one or more output files. The plugin can emit multiple
files; primate writes each. (Most plugins emit just one.)
- `mappings` — symbol → generated-file location, used to populate the
sourcemap so the LSP can jump from `.prim` to generated code.
- `errors` — non-empty array signals failure. Each entry has `message`
and an optional `source` (file/line/column).
## Type and value JSON shapes
Type expressions are tagged unions:
```json
{ "kind": "u32" }
{ "kind": "string" }
{ "kind": "array", "element": { "kind": "u32" } }
{ "kind": "fixed_array", "element": { "kind": "u32" }, "length": 3 }
{ "kind": "map", "key": { "kind": "string" }, "value": { "kind": "u32" } }
{ "kind": "tuple", "elements": [ ... ] }
{ "kind": "optional", "inner": { "kind": "string" } }
{ "kind": "enum", "name": "LogLevel", "namespace": "logging" }
{ "kind": "alias", "name": "Port", "namespace": "network" }
```
Values are untagged (each shape has a uniquely-typed payload):
```json
8 // integer
3.14 // float
true // bool
"hello" // string
{ "nanoseconds": 30000000000 } // duration
[1, 2, 3] // array / fixed-array / tuple
{ "key": value, ... } // map (untagged JSON object)
null // optional, none case
{ "variant": "Info", "value": 1 } // enum
```
Plugins typically deserialize into language-native types and re-serialize
to the target syntax.
## Wiring up a plugin
In `primate.toml`:
```toml
[[output]]
generator = "lua"
path = "scripts/generated/constants.lua"
command = "/usr/local/bin/primate-lua" # absolute or on $PATH
options.naming = "camelCase"
```
Or, if your plugin is a script in the project:
```toml
[[output]]
generator = "lua"
path = "scripts/generated/constants.lua"
command = ["python", "scripts/primate_lua.py"]
```
`path` can be a single file or a directory — that's up to your
plugin. The plugin decides how many files to emit and what their
relative paths are; it's purely a convention between you and your
generator.
primate runs the command with stdin/stdout/stderr piped, sends the
request, waits for the response, and writes whatever the response says.
## See also
- [Writing a generator](./writing-a-generator.md) — worked example.
- [`primate build`](../cli/build.md) — invokes plugins.