rbnx/cmd/mod.rs
1// SPDX-License-Identifier: MulanPSL-2.0
2// Command Module
3//
4// Command definitions and execution for robonix-cli
5
6use anyhow::Result;
7use clap::Subcommand;
8use std::path::PathBuf;
9
10use robonix_cli::Config;
11
12mod ask;
13mod build;
14mod chat;
15mod clean;
16mod codegen;
17mod config;
18mod deploy;
19mod info;
20mod init;
21mod inspect;
22mod install;
23mod list;
24mod package_new;
25mod path;
26mod run_package;
27mod setup;
28mod shutdown;
29mod teardown;
30mod validate;
31
32const DEFAULT_ENDPOINT: &str = "localhost:50051";
33
34#[derive(Subcommand)]
35pub enum Commands {
36 /// Build a package (local path or system-installed)
37 Build {
38 /// Local package path (relative to $RBNX_INVOCATION_CWD, else process cwd)
39 #[arg(short = 'p', long)]
40 path: Option<PathBuf>,
41 /// Build by system-installed package name
42 #[arg(short = 'g', long)]
43 global: Option<String>,
44 /// Clean build (remove rbnx-build before building). Default: incremental.
45 #[arg(long)]
46 clean: bool,
47 },
48 /// Start the package (runs its single top-level `start` shell block).
49 /// Defaults to the package containing the current directory when `-p`
50 /// is omitted. Blocks until the process exits. The pre-dev-packaging
51 /// `-n / --node` flag is gone — one package = one start body now.
52 Start {
53 /// Package path or installed name; relative paths use $RBNX_INVOCATION_CWD, else process cwd.
54 /// If omitted, rbnx walks up from the current directory to find a package manifest.
55 #[arg(short = 'p', long)]
56 package: Option<String>,
57 /// Registry endpoint (default: 127.0.0.1:50051)
58 #[arg(long)]
59 endpoint: Option<String>,
60 /// Per-instance config file (JSON or YAML). Materialized into
61 /// `RBNX_CONFIG_FILE` for the start body. Same shape as the per-
62 /// package `config:` block under a deploy `robonix_manifest.yaml`
63 /// — `rbnx boot` writes one of these per package and re-execs
64 /// `rbnx start --config <file>` internally.
65 #[arg(short = 'c', long)]
66 config: Option<PathBuf>,
67 /// Inline config overrides. Repeatable, dotted-path keys, e.g.
68 /// `--set sensors.lidar2d=true --set algo=rtabmap`. Layered on
69 /// top of `--config` (sets win). Values are JSON-parsed when
70 /// possible (so `--set max_speed=0.5` is a number, `--set on=true`
71 /// is a bool); fall back to a string when JSON parsing fails.
72 #[arg(short = 's', long = "set", value_name = "KEY=VALUE")]
73 set: Vec<String>,
74 },
75 /// Boot the whole stack from a top-level `robonix_manifest.yaml` — system
76 /// services (atlas/executor/pilot/liaison/memory/vlm) plus every package
77 /// declared under `primitive`/`service`/`skill`. Blocks until Ctrl-C.
78 /// `rbnx deploy` is kept as an alias for back-compat.
79 #[command(alias = "deploy")]
80 Boot {
81 /// Path to the deployment manifest (default: `./robonix_manifest.yaml`).
82 #[arg(short = 'f', long, default_value = "robonix_manifest.yaml")]
83 file: PathBuf,
84 /// Directory for per-component logs (default: `<manifest-dir>/rbnx-boot/logs`).
85 #[arg(long)]
86 log_dir: Option<PathBuf>,
87 /// Skip starting the `system:` block (atlas/pilot/etc). Useful when
88 /// those are already running externally.
89 #[arg(long)]
90 skip_system: bool,
91 },
92 /// Tear down a stack previously brought up by `rbnx boot`. Reads the
93 /// per-manifest state file boot writes (`<manifest-dir>/rbnx-boot/state.json`)
94 /// to know which process groups + docker containers to kill, so the
95 /// host doesn't accumulate orphaned drivers when boot dies on an error
96 /// path or its parent shell window is closed.
97 Shutdown {
98 /// Path to the deployment manifest (default: `./robonix_manifest.yaml`).
99 #[arg(short = 'f', long, default_value = "robonix_manifest.yaml")]
100 file: PathBuf,
101 },
102 /// Drop build artifacts. Per-package: `rbnx clean -p <pkg>` removes
103 /// `<pkg>/rbnx-build/`. Per-deploy: `rbnx clean -f <manifest>` recurses
104 /// over every package the manifest references (path: + url: + system/*),
105 /// wipes each one's `rbnx-build/`, and clears the deploy's
106 /// `rbnx-boot/{logs,state.json}`. Pass `--cache` to also wipe
107 /// `rbnx-boot/cache/` (forces re-clone of url:-fetched packages on next
108 /// boot). Defaults to the package containing the current directory when
109 /// neither `-p` nor `-f` is given.
110 Clean {
111 /// Package path (defaults to walking up from cwd).
112 #[arg(short = 'p', long)]
113 package: Option<PathBuf>,
114 /// Deploy manifest path. When set, recurses over the manifest.
115 #[arg(short = 'f', long)]
116 file: Option<PathBuf>,
117 /// With `-f`, also wipe `rbnx-boot/cache/` (force re-clone).
118 #[arg(long)]
119 cache: bool,
120 },
121 /// Install a package from GitHub or local path
122 Install {
123 /// Install from GitHub (e.g. user/repo or https://github.com/user/repo)
124 #[arg(long)]
125 github: Option<String>,
126 /// Install from local path
127 #[arg(long)]
128 path: Option<PathBuf>,
129 },
130 /// List system-installed packages
131 List,
132 /// Show details of a system-installed package
133 Info {
134 /// Package name
135 name: String,
136 },
137 /// Validate a package manifest without building. If no path is given,
138 /// rbnx walks up from the current directory to find a package manifest.
139 Validate {
140 /// Package directory (relative paths use $RBNX_INVOCATION_CWD, else process cwd)
141 path: Option<PathBuf>,
142 },
143 /// Configure robonix-cli
144 Config {
145 /// Set package storage path
146 #[arg(short = 'p', long)]
147 set_storage_path: Option<PathBuf>,
148 /// Show current configuration
149 #[arg(short, long)]
150 show: bool,
151 },
152 /// Run codegen for a package (wraps robonix-codegen + grpc_tools.protoc).
153 /// Stages system protos under `<pkg>/rbnx-build/proto-staging/`, then emits
154 /// `<pkg>/proto_gen/` (and optional `<pkg>/robonix_mcp_types/`). Replaces the
155 /// copy-pasted boilerplate in package build.sh scripts. If `-p` is omitted,
156 /// rbnx walks up from the current directory to find a package manifest.
157 Codegen {
158 /// Package path (relative to $RBNX_INVOCATION_CWD, else process cwd)
159 #[arg(short = 'p', long)]
160 package: Option<PathBuf>,
161 /// Also generate robonix_mcp_types/ (for MCP-based packages)
162 #[arg(long)]
163 mcp: bool,
164 /// Also generate ros2_idl/ — the canonical ROS 2 message overlay
165 /// (source). Build it with `colcon build` in a ROS 2 environment and
166 /// source install/setup.bash so rclpy types are Robonix's.
167 #[arg(long)]
168 ros2: bool,
169 /// Remove previous proto_gen/, robonix_mcp_types/, rbnx-build/ before regenerating
170 #[arg(long)]
171 clean: bool,
172 /// Directory (relative to package root, or absolute) where proto_gen/ and robonix_mcp_types/
173 /// should be placed. Defaults to package root; use e.g. `--out-dir tiago_bridge` to put
174 /// stubs inside a package subdirectory.
175 #[arg(long)]
176 out_dir: Option<PathBuf>,
177 },
178 /// Register this directory as the robonix source tree (persists to ~/.robonix/config.yaml).
179 /// Call once from a cloned robonix repo so packages anywhere on disk can find capabilities/IDL.
180 Setup {
181 /// Path to the robonix repo root (default: $RBNX_INVOCATION_CWD or process cwd).
182 /// If the given path is a sub-directory, walks up to find the root.
183 path: Option<PathBuf>,
184 },
185 /// Print an absolute path rooted in the configured robonix source tree (for build scripts).
186 /// Keys: root, rust, capabilities, interfaces-lib, runtime-proto, robonix-api
187 Path {
188 /// Path key to resolve (see above).
189 key: String,
190 },
191
192 /// List all registered capabilities (one row per provider by default;
193 /// pass -v to expand the per-provider capability list, lspci -tv style)
194 #[command(alias = "nodes")]
195 Caps {
196 /// robonix-atlas endpoint
197 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
198 server: String,
199 /// Output as JSON (forces full detail regardless of -v)
200 #[arg(long)]
201 json: bool,
202 /// Expand each provider's capability list; without this, only the
203 /// summary header line per provider is printed.
204 #[arg(short = 'v', long)]
205 verbose: bool,
206 },
207 /// List atlas's loaded contract registry (every `<root>/capabilities/**/*.toml`
208 /// atlas parsed at startup). Pass -v for field-level schemas + source paths,
209 /// -p / --prefix to filter by namespace prefix.
210 Contracts {
211 /// robonix-atlas endpoint
212 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
213 server: String,
214 /// Filter by id prefix (e.g. `robonix/primitive/camera/`)
215 #[arg(short = 'p', long)]
216 prefix: Option<String>,
217 /// Output as JSON (forces full detail)
218 #[arg(long)]
219 json: bool,
220 /// Expand each contract's field schema + source toml path
221 #[arg(short = 'v', long)]
222 verbose: bool,
223 },
224 /// Show CAPABILITY.md for registered providers (all, or one with --provider)
225 Describe {
226 /// robonix-atlas endpoint
227 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
228 server: String,
229 /// Show full CAPABILITY.md content for a specific provider_id
230 #[arg(long, alias = "node")]
231 provider: Option<String>,
232 /// Output as JSON
233 #[arg(long)]
234 json: bool,
235 },
236 /// Print every MCP-callable tool visible to the agent (executor builtins + provider capabilities)
237 Tools {
238 /// robonix-atlas endpoint
239 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
240 server: String,
241 /// Output as JSON
242 #[arg(long)]
243 json: bool,
244 },
245 /// Show active channels (consumer→provider connections opened via ConnectCapability)
246 Channels {
247 /// robonix-atlas endpoint
248 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
249 server: String,
250 },
251 /// Dump full runtime state as JSON (providers, capabilities, channels)
252 Inspect {
253 /// robonix-atlas endpoint
254 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
255 server: String,
256 },
257
258 /// Chat with the Robonix agent in an interactive TUI
259 Chat {
260 /// robonix-atlas endpoint (used to discover agent)
261 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
262 server: String,
263 },
264
265 /// Initialize a new robonix project (creates robonix_manifest.yaml + directory skeleton)
266 Init {
267 /// Project name (also used as directory name)
268 name: String,
269 /// Parent directory (default: current directory)
270 #[arg(long)]
271 path: Option<PathBuf>,
272 },
273
274 /// Create a new package under the appropriate role directory
275 PackageNew {
276 /// Package name
277 name: String,
278 /// Package type: primitive, service, or skill
279 #[arg(short = 't', long = "type", default_value = "service")]
280 pkg_type: String,
281 /// Target package directory to create (when given, --type is ignored)
282 #[arg(long)]
283 path: Option<PathBuf>,
284 },
285
286 /// One-shot non-interactive prompt — same gRPC path as `rbnx chat`
287 /// (atlas connect → SubmitTask → stream PilotEvent), but prints
288 /// events to stdout and exits when the stream closes. Useful for
289 /// scripted tests / CI / agent-driven runs where stdout is the
290 /// artifact.
291 Ask {
292 /// The user message to send to the pilot.
293 prompt: String,
294 /// robonix-atlas endpoint
295 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
296 server: String,
297 /// Emit one JSON object per pilot event on stdout (line-delimited).
298 /// Default is human-readable text with tool-call summaries.
299 #[arg(long)]
300 json: bool,
301 },
302}
303
304pub async fn execute(command: Commands, config: Config) -> Result<()> {
305 match command {
306 Commands::Build {
307 path,
308 global,
309 clean,
310 } => run_package::execute_build(config, path, global, clean).await,
311 Commands::Start {
312 package,
313 endpoint,
314 config: cfg_file,
315 set,
316 } => {
317 run_package::execute_start(
318 &config,
319 package.as_deref(),
320 endpoint.as_deref(),
321 cfg_file.as_deref(),
322 &set,
323 )
324 .await
325 }
326 Commands::Boot {
327 file,
328 log_dir,
329 skip_system,
330 } => deploy::execute(config, file, log_dir, skip_system).await,
331 Commands::Shutdown { file } => shutdown::execute(file).await,
332 Commands::Clean {
333 package,
334 file,
335 cache,
336 } => clean::execute(config, package, file, cache).await,
337 Commands::Install { github, path } => install::execute(config, github, path).await,
338 Commands::List => list::execute(config).await,
339 Commands::Info { name } => info::execute(config, &name).await,
340 Commands::Validate { path } => validate::execute(path).await,
341 Commands::Config {
342 set_storage_path,
343 show,
344 } => config::execute(config, set_storage_path, show).await,
345 Commands::Codegen {
346 package,
347 mcp,
348 ros2,
349 clean,
350 out_dir,
351 } => codegen::execute(config, package, mcp, ros2, clean, out_dir).await,
352 Commands::Setup { path } => setup::execute(config, path).await,
353 Commands::Path { key } => path::execute(config, key).await,
354 Commands::Caps {
355 server,
356 json,
357 verbose,
358 } => inspect::providers(&server, json, verbose).await,
359 Commands::Contracts {
360 server,
361 prefix,
362 json,
363 verbose,
364 } => inspect::contracts(&server, prefix.as_deref(), json, verbose).await,
365 Commands::Describe {
366 server,
367 provider,
368 json,
369 } => inspect::describe(&server, provider.as_deref(), json).await,
370 Commands::Tools { server, json } => inspect::tools(&server, json).await,
371 Commands::Channels { server } => inspect::channels(&server).await,
372 Commands::Inspect { server } => inspect::inspect(&server).await,
373 Commands::Chat { server } => chat::execute(&server).await,
374 Commands::Init { name, path } => init::execute(&name, path.as_deref()).await,
375 Commands::PackageNew {
376 name,
377 pkg_type,
378 path,
379 } => package_new::execute(&name, &pkg_type, path.as_deref()).await,
380 Commands::Ask {
381 prompt,
382 server,
383 json,
384 } => ask::execute(&server, &prompt, json).await,
385 }
386}