cerulion binary groups all functionality under top-level commands:
<required>, [optional], {a|b} (choice), and ... (repeatable). Synopsis lines are not directly runnable; each command includes a separate runnable example.
Global
cerulion prints Error: <msg> to stderr and exits with a failure code.
Most commands require a workspace, discovered by walking upward for a Cargo.toml containing [workspace] plus a graphs/ directory. workspace create/init, tui, trace, clean, completions, login, account, pair, connect, viz, bag, flashback, and ros2 run/launch/migrate do not require discovery; schema info and schema list use a workspace when they find one and fall back to the built-in types when they do not. The topic commands use transport discovery and need no workspace.
workspace
cerulion workspace create
cerulion workspace create
./<name>/ with graphs/, nodes/, schemas/, Cargo.toml ([workspace], members=["nodes/*"], resolver="2", workspace dependencies for cerulion_core and native_ros2_messages), .cargo/config.toml, and a .gitignore carrying .cerulion/ (the workspace lock directory). Errors if ./<name> already exists.The cerulion_core and native_ros2_messages dependencies point at published crates, exact-pinned to the CLI’s own version, unless your cerulion binary was built from a cerulion-base checkout — then they are absolute path dependencies into that checkout. The decision keys on where the binary lives, never on the current directory. The command prints which it chose on the line under the created path.cerulion workspace init
cerulion workspace init
Cargo.toml with [workspace] already exists there. The workspace name is the directory’s file name.node
Subcommands:create, delete, modify, build, stage, run, list, info. All require workspace discovery.
cerulion node create
cerulion node create
SCHEMA accepts sensor_msgs/Image, sensor_msgs::Image, or a bare Image. Bare names resolve against your workspace schemas first, then the built-in ROS 2 messages; an ambiguous bare name errors and lists the candidates.Generated files: nodes/<type>/Cargo.toml (cdylib) and nodes/<type>/src/lib.rs.A source-only node (no -i/-T) must declare a non-data policy — --policy period_ms=N or --policy external.cerulion node delete
cerulion node delete
nodes/<node_type>/ (recursive) and the workspace member entry.cerulion node modify
cerulion node modify
src/lib.rs. Your tick() body is untouched. --raw-ffi is not accepted here — the existing node form is preserved.cerulion node build
cerulion node build
cargo build for that crate with the cdylib feature, producing .so (Linux) or .dylib (macOS). CARGO_TARGET_DIR is honored.If the crate declares optional system dependencies, the build probes them with pkg-config first and enables each feature whose libraries are present; a missing one is reported loudly and the build continues without that feature.cerulion node stage
cerulion node stage
SOURCE is either relative (sensor/image → /<prefix>/sensor/image) or absolute (/vendor/camera/image, an external topic with no in-graph producer).cerulion node run
cerulion node run
topic echo. Networking follows the same defaults as graph run.cerulion node list
cerulion node list
TYPE INPUTS OUTPUTS POLICY table derived from each crate’s src/lib.rs.cerulion node info
cerulion node info
graph
Subcommands:create, run, validate, list, levels, profile, partition.
cerulion graph create
cerulion graph create
cerulion graph run
cerulion graph run
Ctrl+C or a node calls request_shutdown(). On Unix under the real clock, an unpartitioned graph is executed multi-process by default — see Run a graph across processes.A run that stops normally spends a bounded pass of roughly two seconds on cleanup after the graph is over, reclaiming shared-memory state left behind by dead nodes; because the graph has already stopped, nothing you are waiting on is delayed. Whatever the budget does not reach stays for the next pass. A run killed with SIGKILL skips that pass, and the next run that stops normally picks up what it left. A run refused before it starts — an unknown graph name, for instance — does no cleanup at all.cerulion graph validate
cerulion graph validate
9/9 checks passed. Exits non-zero on any failure, which makes it the command to run in CI.Node libraries must be built first; an unbuilt node fails with cdylib '<type>' not found.cerulion graph list
cerulion graph list
cerulion graph levels
cerulion graph levels
process_groups: — which group covers which levels. Exits non-zero if the declared partition is invalid.The partition verdict names every split block edge it found creditable, so this is the verb that answers whether a hand-written split will run:cerulion graph profile
cerulion graph profile
graph partition consumes. Nodes that never sampled enough fires are reported as isolated rather than guessed at.cerulion graph partition
cerulion graph partition
process_groups: block with a .bak backup alongside it.topic
Topic commands attach to the running transport and need no workspace.cerulion topic list
cerulion topic list
ROBOTS and REMOTE TOPICS sections. A failed remote query is a loud note and still exits 0.cerulion topic info
cerulion topic info
cerulion topic echo
cerulion topic echo
Ctrl+C. A name that is not a local topic is looked for on the LAN and demanded for the length of the command; CERULION_NETWORK=off keeps it local-only.cerulion topic hz
cerulion topic hz
schema
cerulion schema create
cerulion schema create
schemas/<name>.yaml — the name verbatim, no case change — holding a schemas: skeleton whose single entry key is the PascalCased name. cerulion schema create detections writes schemas/detections.yaml containing a Detections: entry. See Graph and schema files.cerulion schema delete
cerulion schema delete
cerulion schema info
cerulion schema info
schemas/*.yaml, your workspace’s .msg store (schemas/<package>/msg/<Type>.msg), the built-in ROS 2 types, and a connected robot’s own types. The source: line names which one answered.cerulion schema list
cerulion schema list
.msg store, then every built-in ROS 2 message grouped by package. A built-in that a .msg store entry overrides is listed as <name> (shadowed by msg store '<name>').bag
Recording and replay. See Record and replay a run.cerulion bag record
cerulion bag record
bag play needs and enough for topic echo to decode from the workspace schema store.--resim compatibility is not claimed for a bag record bag, --run included. A standalone capture carries frames and no scheduler trace at all; a --run attach does carry the run’s effective graph, environment, manifests and a trace from its attach point, but its descriptors are still observability-grade. When you know you will re-execute, record the run with graph run --record.cerulion bag info
cerulion bag info
absorbance block: per topic, whether the queue its recording tap was given holds that topic’s frames through the run’s own worst drain gap. Every topic with a verdict is counted; only the actionable ones are named — short, no claim, a stall the drain-gap histogram cannot rank, a pass on a floor rate, or a topic that fell short earlier and recovered. On a flashback capture the block describes the recorder that took the capture, over its whole run, not the captured window. See Record and replay a run.cerulion bag play
cerulion bag play
--report and --tolerance need --verify, not merely --resim. A bare --resim all re-executes the graph without comparing anything, so there is no verdict for either flag to describe, and the command refuses rather than writing an empty report.cerulion bag migrate
cerulion bag migrate
--resim accepts it.--yes writes without asking, and is required when stdin is not a terminal; --dry-run writes nothing). The input bag is never modified, and every frame and attachment is copied through unchanged. A bag whose graph already parses is refused — there is nothing to migrate.flashback
recordings/flashbacks/. There is nothing to arm in advance — a live run already holds the rolling window. CERULION_FLASHBACK=off turns the capture plane off.
viz
connect
cerulion topic echo, topic hz, and Cerulion Studio see them like local topics. With no --topic and no --all, it prints the robot’s catalog and demands nothing.
pair
name → endpoint id in ~/.cerulion/robots.toml. Your desk key is created once at ~/.cerulion/desk.key and never overwritten.
Exit codes: 0 paired · 1 usage error · 2 refused · 3 unreachable · 4 wrong code · 130 interrupted.
--code: a code on the command line is visible in process listings while the ceremony runs.
ros2
run and launch put stock ROS 2 entry points on Cerulion transport, migrate rewrites a colcon workspace’s C++ publish sites to the loaned-message API, and attach bridges a robot that is already running ROS 2.
run and launch
Runs a stock ROS 2 entry point on Cerulion transport — swap the command, not the stack.ros2 run demo_nodes_cpp talker becomes cerulion ros2 run demo_nodes_cpp talker; ros2 launch my_robot_bringup camera.launch.py becomes cerulion ros2 launch my_robot_bringup camera.launch.py. Unix only.
Both verbs are verbatim pass-through wrappers: everything after run or launch is forwarded to the native ros2 run / ros2 launch untouched — hyphenated flags, the package form, launch arguments. Their argument surface is ROS 2’s, so consult ros2 run --help / ros2 launch --help, and a bad package name or a missing launch file is ros2’s own error.
Before forwarding, Cerulion stages the child environment:
exec()s ros2 <verb> [ARGS...]: the process becomes ros2, so stdout, stderr, Ctrl+C, and the exit code pass through the kernel untouched, and a successful exec inherits ros2’s own exit code.
The nodes keep their ROS names. A ROS 2 publisher on /camera/image_raw is the Cerulion topic /camera/image_raw — the fully-qualified name verbatim, leading slash included, for topics and for services alike. Nothing is prefixed, stripped, or aliased, so a native #[input] whose source: is /camera/image_raw reads that publisher directly, and a recording of the run carries the topic under the name ROS 2 uses.
Each ROS 2 publisher also registers its topic with the machine’s network plane, so once that plane is up the topic is announced and a remote desk can demand it like a native node’s output — see Reach remote robots. Registration is best-effort and never blocks local ROS 2 pub/sub: a topic that could not be registered stays local-only and says so in a warning. Subscriptions register nothing, and a registration is not withdrawn when a publisher is destroyed — the topic stays announced, with no producer behind it, for the life of the process.
Exit codes belong to Cerulion only when it never reached the exec: 69 librmw_cerulion.so missing (the message carries the build command and the override), or --adopt-take given · 127 ros2 not on PATH · 2 a usage error, such as a bare cerulion ros2, or CERULION_RMW_ADOPT_TAKE set in the environment · 1 any other pre-exec failure.
ros2: entries and run cerulion graph run — see Graph and schema files.
migrate
Rewrites this colcon workspace’s C++ publish call sites to the ROS 2 loaned-message API (borrow_loaned_message() → fill → publish(std::move(loaned))) wherever a clang AST prover shows the rewrite is behavior-preserving. It is an AST transform over the workspace’s compile_commands.json, never a regex; anything it cannot prove lands in a manual-candidates report with the reason. rclpy nodes are report-only, because rclpy has no loaned-message API upstream.
rclcpp falls back to allocate-and-copy when the rmw cannot loan.
Dry run is the default. It prints the full unified diff and the candidates report, and refreshes the machine-readable manifest at .cerulion/ros2-migrate-manifest.json — the only thing it writes.
--write refuses a dirty git tree, then writes one commit plus cerulion-ros2-migration.patch and runs colcon build --packages-select over the affected packages. Undo with git revert <sha>, or git apply -R cerulion-ros2-migration.patch. If a pre-commit hook stages paths of its own, it stages them inside git commit — after this verb’s index check — so they land in the migration commit; the run warns and names them, and git apply -R is then the safe undo, because it reverses only the migration’s own edits.
Requires a compile database (colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) and the migration engine binary cerulion-ros2-migrate-clang, found beside the cerulion binary, on PATH, or via CERULION_ROS2_MIGRATE_TOOL. Exit 69 with build instructions when it is absent.
attach
graphs/<name>.yaml plus a bridge configuration for the ones whose types resolve, and runs the bridge. See Bridge a ROS 2 system.
Every run’s report ends with a MIGRATION section — there is no flag for it, and --dry-run prints it too. It groups the discovered processes (one DDS participant each) by what a restart under rmw_cerulion would buy: restartable today, when every message type on the process’s endpoints resolves locally; restartable once consent writes the schemas this attach resolved; or stays bridged, with the unresolvable types named. Endpoints with no ros_discovery_info record are counted as topics, not processes, and are reported as absence of evidence — a vendor or raw-DDS process, or a node table the window did not observe. ROS plumbing endpoints (parameter services, /rosout, /parameter_events, ros_discovery_info) are excluded from the judgment. The section then prints the equivalent cerulion ros2 launch line, a paste-ready ros2: graph block with <package>/<executable> placeholders, the bridged-versus-native cost facts, and a closing pointer at cerulion ros2 migrate. It is rendered from the discovery data already in hand: no extra network, no change to the outcome or exit code.
login and account
cerulion login signs this machine in with a device-code flow: it prints a short code and a verification URL, then waits for you to authorize it in the browser. The first command that needs an identity triggers it for you.
account devices lists and revokes the machines registered to your account. Robot access management lives in Cerulion Studio and the web account page, not the CLI.
completions
bash, elvish, fish, powershell, zsh. Completion covers subcommands, flags, and enum values, plus live names — topics, node types, graph names, schemas, paired robots, and .mcap files. A completion never opens the network.
tui
q.
trace
<topic> seq=<N> t=<TIMESTAMP_NS> schema=<HASH> — from the JSONL publish trace (trace_*.jsonl) in a directory. -t restricts to one topic by exact name, -n limits the count, and -r reverses to most-recent-first. This is not the MCAP bag — for recordings use cerulion bag.
clean
--report-only reports what is reclaimable and deletes nothing, though the dead-node sweep still runs.
When the dead-node sweep cannot clear every dead node, clean reports the leftover shared-memory state but does not reclaim it: a dead node that is still registered needs its state to be reaped later, so removing it would strand that node for good. Clear the reported failures, then run cerulion clean again.
You rarely need this command — graph run sweeps dead nodes at start, and cleans up again when the graph exits normally.