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
{
"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. Currently1.outputPath— theoutputvalue from the relevant[generators.*]section ofprimate.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
{
"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.primto generated code.errors— non-empty array signals failure. Each entry hasmessageand an optionalsource(file/line/column).
Type and value JSON shapes
Type expressions are tagged unions:
{ "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):
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:
[[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:
[[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 — worked example.
primate build— invokes plugins.