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;
13pub(crate) mod boot_watchdog;
14mod build;
15mod chat;
16mod check_remotes;
17mod clean;
18mod codegen;
19mod config;
20mod deploy;
21mod docs;
22mod info;
23mod init;
24mod inspect;
25mod install;
26mod list;
27mod logs;
28mod package_new;
29mod path;
30mod run_package;
31mod setup;
32mod shutdown;
33mod teardown;
34mod update;
35mod validate;
36
37const DEFAULT_ENDPOINT: &str = "localhost:50051";
38
39#[derive(Subcommand)]
40pub enum Commands {
41 /// Build a package (local path or system-installed)
42 Build {
43 /// Deployment manifest to build (builds every declared package)
44 #[arg(short = 'f', long, value_name = "FILE", conflicts_with_all = ["path", "global"])]
45 file: Option<PathBuf>,
46 /// Local package path (relative to $RBNX_INVOCATION_CWD, else process cwd)
47 #[arg(short = 'p', long)]
48 path: Option<PathBuf>,
49 /// Build by system-installed package name
50 #[arg(short = 'g', long)]
51 global: Option<String>,
52 /// Clean build (remove rbnx-build before building). Default: incremental.
53 #[arg(long)]
54 clean: bool,
55 /// Skip the remote-provider freshness check before building cached packages.
56 /// Useful when offline or when the deployment cache is intentionally pinned.
57 #[arg(long)]
58 no_update_check: bool,
59 },
60 /// Start one package (runs its `start` block; blocks until it exits)
61 ///
62 /// Defaults to the package containing the current directory when `-p`
63 /// is omitted. The pre-dev-packaging `-n / --node` flag is gone — one
64 /// package = one start body now.
65 Start {
66 /// Package path or installed name; relative paths use $RBNX_INVOCATION_CWD, else process cwd.
67 /// If omitted, rbnx walks up from the current directory to find a package manifest.
68 #[arg(short = 'p', long)]
69 package: Option<String>,
70 /// Registry endpoint (default: 127.0.0.1:50051)
71 #[arg(long)]
72 endpoint: Option<String>,
73 /// Per-instance config file (JSON or YAML). Decoded and delivered to
74 /// the provider through Driver(CMD_INIT). It has the same shape as a
75 /// package's nested `config:` block in `robonix_manifest.yaml`.
76 #[arg(short = 'c', long)]
77 config: Option<PathBuf>,
78 /// Inline config overrides. Repeatable, dotted-path keys, e.g.
79 /// `--set sensors.lidar2d=true --set algo=rtabmap`. Layered on
80 /// top of `--config` (sets win). Values are JSON-parsed when
81 /// possible (so `--set max_speed=0.5` is a number, `--set on=true`
82 /// is a bool); fall back to a string when JSON parsing fails.
83 #[arg(short = 's', long = "set", value_name = "KEY=VALUE")]
84 set: Vec<String>,
85 /// Package manifest filename to use instead of the default
86 /// `package_manifest.yaml`. Lets a package ship per-deployment-target
87 /// variants (e.g. `package_manifest.jetson-native.yaml`). `rbnx boot`
88 /// passes this through from a deploy entry's `manifest:` field.
89 #[arg(short = 'm', long)]
90 manifest: Option<String>,
91 },
92 /// Boot the whole stack from a `robonix_manifest.yaml` (until Ctrl-C)
93 ///
94 /// Brings up the system services (atlas/executor/pilot/liaison/memory/vlm)
95 /// plus every package declared under `primitive`/`service`/`skill`.
96 /// `rbnx deploy` is kept as an alias for back-compat.
97 #[command(alias = "deploy")]
98 Boot {
99 /// Path to the deployment manifest (default: `./robonix_manifest.yaml`).
100 #[arg(short = 'f', long, default_value = "robonix_manifest.yaml")]
101 file: PathBuf,
102 /// Directory for per-component logs (default: `<manifest-dir>/rbnx-boot/logs`).
103 #[arg(long)]
104 log_dir: Option<PathBuf>,
105 /// Skip starting the `system:` block (atlas/pilot/etc). Useful when
106 /// those are already running externally.
107 #[arg(long)]
108 skip_system: bool,
109 /// Skip the remote-provider freshness check (the per-package
110 /// `git fetch` pass that runs before boot). Use when offline or in a
111 /// hurry.
112 #[arg(long)]
113 no_update_check: bool,
114 /// Stream append-only, Scribe-backed component logs during boot.
115 /// Disables animated cursor updates so output can be read or piped
116 /// like a Linux/FreeBSD kernel boot log or Android logcat.
117 #[arg(short, long)]
118 verbose: bool,
119 },
120 /// Internal detached cleanup process for one `rbnx boot` invocation.
121 #[command(name = "__watch-boot", hide = true)]
122 WatchBoot {
123 #[arg(long)]
124 state: PathBuf,
125 #[arg(long)]
126 boot_pid: u32,
127 #[arg(long)]
128 boot_start_time_ticks: Option<u64>,
129 #[arg(long)]
130 boot_id: String,
131 },
132 /// Update remote (`url:`) providers to their latest upstream commit
133 ///
134 /// In a deploy dir (or with `-f <manifest>`) updates ALL cloned remote
135 /// providers; with `-p <dir>` (or inside a package checkout) updates just
136 /// that one. Shows an overview and asks for confirmation before pulling.
137 Update {
138 /// Update a single package checkout at this path.
139 #[arg(short = 'p', long)]
140 path: Option<PathBuf>,
141 /// Deploy manifest whose remote providers to update
142 /// (default: `./robonix_manifest.yaml`).
143 #[arg(short = 'f', long)]
144 file: Option<PathBuf>,
145 },
146 /// Tear down a stack previously brought up by `rbnx boot`
147 ///
148 /// Reads the per-manifest state file boot writes
149 /// (`<manifest-dir>/rbnx-boot/state.json`) to kill the right process
150 /// groups + docker containers, so the host doesn't accumulate orphaned
151 /// drivers when boot dies on an error path or its shell window is closed.
152 Shutdown {
153 /// Path to the deployment manifest (default: `./robonix_manifest.yaml`).
154 #[arg(short = 'f', long, default_value = "robonix_manifest.yaml")]
155 file: PathBuf,
156 },
157 /// Drop build artifacts (`rbnx-build/`), per-package or per-deploy
158 ///
159 /// `rbnx clean -p <pkg>` removes `<pkg>/rbnx-build/`. `rbnx clean -f
160 /// <manifest>` recurses over every package the manifest references
161 /// (path: + url: + system/*), wipes each one's `rbnx-build/`, and clears
162 /// the deploy's `rbnx-boot/{logs,state.json}`. `--cache` also wipes
163 /// `rbnx-boot/cache/` (forces re-clone of url: packages). Defaults to the
164 /// package containing cwd when neither `-p` nor `-f` is given.
165 Clean {
166 /// Package path (defaults to walking up from cwd).
167 #[arg(short = 'p', long)]
168 package: Option<PathBuf>,
169 /// Deploy manifest path. When set, recurses over the manifest.
170 #[arg(short = 'f', long)]
171 file: Option<PathBuf>,
172 /// With `-f`, also wipe `rbnx-boot/cache/` (force re-clone).
173 #[arg(long)]
174 cache: bool,
175 },
176 /// Install a package from GitHub or local path
177 ///
178 /// Legacy system-installed-package tooling; superseded by deploy
179 /// manifests (`rbnx boot`/`build`). Hidden from the command list but
180 /// still functional.
181 #[command(hide = true)]
182 Install {
183 /// Install from GitHub (e.g. user/repo or <https://github.com/user/repo>)
184 #[arg(long)]
185 github: Option<String>,
186 /// Install from local path
187 #[arg(long)]
188 path: Option<PathBuf>,
189 },
190 /// List system-installed packages (legacy; hidden)
191 #[command(hide = true)]
192 List,
193 /// Show details of a system-installed package (legacy; hidden)
194 #[command(hide = true)]
195 Info {
196 /// Package name
197 name: String,
198 },
199 /// Validate a package manifest without building
200 ///
201 /// If no path is given, rbnx walks up from the current directory to find
202 /// a package manifest.
203 Validate {
204 /// Package directory (relative paths use $RBNX_INVOCATION_CWD, else process cwd)
205 path: Option<PathBuf>,
206 },
207 /// Configure robonix-cli
208 Config {
209 /// Set package storage path
210 #[arg(short = 'p', long)]
211 set_storage_path: Option<PathBuf>,
212 /// Show current configuration
213 #[arg(short, long)]
214 show: bool,
215 },
216 /// Run codegen for a package (proto + gRPC stubs + MCP types)
217 ///
218 /// Wraps robonix-codegen + grpc_tools.protoc: stages system protos under
219 /// `<pkg>/rbnx-build/proto-staging/`, then emits `<pkg>/proto_gen/` (and
220 /// optional `<pkg>/robonix_mcp_types/`). If `-p` is omitted, rbnx walks up
221 /// from the current directory to find a package manifest.
222 Codegen {
223 /// Package path (relative to $RBNX_INVOCATION_CWD, else process cwd)
224 #[arg(short = 'p', long)]
225 package: Option<PathBuf>,
226 /// Also generate robonix_mcp_types/ (for MCP-based packages)
227 #[arg(long)]
228 mcp: bool,
229 /// Also generate ros2_idl/ — the canonical ROS 2 message overlay
230 /// (source). Build it with `colcon build` in a ROS 2 environment and
231 /// source install/setup.bash so rclpy types are Robonix's.
232 #[arg(long)]
233 ros2: bool,
234 /// Remove previous proto_gen/, robonix_mcp_types/, rbnx-build/ before regenerating
235 #[arg(long)]
236 clean: bool,
237 /// Directory (relative to package root, or absolute) where proto_gen/ and robonix_mcp_types/
238 /// should be placed. Defaults to package root; use e.g. `--out-dir tiago_bridge` to put
239 /// stubs inside a package subdirectory.
240 #[arg(long)]
241 out_dir: Option<PathBuf>,
242 },
243 /// Regenerate the mdBook contract + ROS IDL reference
244 ///
245 /// Rebuilds `docs/src/reference/{contracts,idl}.md` from `capabilities/`.
246 /// Auto-generated + version-stamped — run after changing any contract or
247 /// IDL so the browsable reference stays in sync.
248 Docs {
249 /// Output directory (default: `<root>/docs/src/reference`).
250 #[arg(long)]
251 out_dir: Option<PathBuf>,
252 },
253 /// Register this directory as the robonix source tree
254 ///
255 /// Persists to ~/.robonix/config.yaml. Call once from a cloned robonix
256 /// repo so packages anywhere on disk can find capabilities/IDL.
257 Setup {
258 /// Path to the robonix repo root (default: $RBNX_INVOCATION_CWD or process cwd).
259 /// If the given path is a sub-directory, walks up to find the root.
260 path: Option<PathBuf>,
261 },
262 /// Print an absolute path in the robonix source tree (for build scripts)
263 ///
264 /// Keys: root, rust, capabilities, interfaces-lib, runtime-proto, robonix-api.
265 Path {
266 /// Path key to resolve (see above).
267 key: String,
268 },
269
270 /// List registered capabilities (one row per provider)
271 ///
272 /// Pass -v to expand the per-provider capability list, lspci -tv style.
273 #[command(alias = "nodes")]
274 Caps {
275 /// robonix-atlas endpoint
276 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
277 server: String,
278 /// Output as JSON (forces full detail regardless of -v)
279 #[arg(long)]
280 json: bool,
281 /// Expand each provider's capability list; without this, only the
282 /// summary header line per provider is printed.
283 #[arg(short = 'v', long)]
284 verbose: bool,
285 },
286 /// List atlas's loaded contract registry
287 ///
288 /// Every `<root>/capabilities/**/*.toml` atlas parsed at startup. -v for
289 /// field-level schemas + source paths; -p/--prefix filters by namespace.
290 Contracts {
291 /// robonix-atlas endpoint
292 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
293 server: String,
294 /// Filter by id prefix (e.g. `robonix/primitive/camera/`)
295 #[arg(short = 'p', long)]
296 prefix: Option<String>,
297 /// Output as JSON (forces full detail)
298 #[arg(long)]
299 json: bool,
300 /// Expand each contract's field schema + source toml path
301 #[arg(short = 'v', long)]
302 verbose: bool,
303 },
304 /// Show CAPABILITY.md for registered providers (all, or one with --provider)
305 Describe {
306 /// robonix-atlas endpoint
307 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
308 server: String,
309 /// Show full CAPABILITY.md content for a specific provider_id
310 #[arg(long, alias = "node")]
311 provider: Option<String>,
312 /// Output as JSON
313 #[arg(long)]
314 json: bool,
315 },
316 /// Print every MCP-callable tool visible to the agent (executor builtins + provider capabilities)
317 Tools {
318 /// robonix-atlas endpoint
319 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
320 server: String,
321 /// Output as JSON
322 #[arg(long)]
323 json: bool,
324 },
325 /// Show active channels (consumer→provider connections opened via ConnectCapability)
326 Channels {
327 /// robonix-atlas endpoint
328 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
329 server: String,
330 },
331 /// Dump full runtime state as JSON (providers, capabilities, channels)
332 Inspect {
333 /// robonix-atlas endpoint
334 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
335 server: String,
336 },
337
338 /// Chat with the Robonix agent in an interactive TUI
339 Chat {
340 /// robonix-atlas endpoint (used to discover agent)
341 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
342 server: String,
343 },
344
345 /// Initialize a new robot deployment directory (creates robonix_manifest.yaml)
346 Init {
347 /// Robot deployment directory name
348 name: String,
349 /// Parent directory (default: current directory)
350 #[arg(long)]
351 path: Option<PathBuf>,
352 },
353
354 /// Create a new package under the appropriate role directory
355 PackageNew {
356 /// Package name
357 name: String,
358 /// Package type: primitive, service, or skill
359 #[arg(short = 't', long = "type", default_value = "service")]
360 pkg_type: String,
361 /// Target package directory to create (when given, --type is ignored)
362 #[arg(long)]
363 path: Option<PathBuf>,
364 },
365
366 /// One-shot non-interactive prompt to the agent (stdout, then exit)
367 ///
368 /// Same gRPC path as `rbnx chat` (atlas connect → SubmitTask → stream
369 /// PilotEvent) but prints events to stdout and exits when the stream
370 /// closes. Useful for scripted tests / CI / agent-driven runs.
371 Ask {
372 /// The user message to send to the pilot.
373 prompt: String,
374 /// robonix-atlas endpoint
375 #[arg(long, env = "ROBONIX_ATLAS", default_value = DEFAULT_ENDPOINT)]
376 server: String,
377 /// Emit one JSON object per pilot event on stdout (line-delimited).
378 /// Default is human-readable text with tool-call summaries.
379 #[arg(long)]
380 json: bool,
381 },
382 /// Read Scribe JSON-lines log files and render them with optional
383 /// tag / level filtering. Point at a log directory or read the
384 /// default `<manifest-dir>/rbnx-boot/logs`.
385 Logs {
386 /// Log directory (default: `./rbnx-boot/logs` or `$SCRIBE_LOG_DIR`).
387 #[arg(short = 'd', long)]
388 log_dir: Option<PathBuf>,
389 /// Filter to one or more tags (OR semantics).
390 #[arg(short = 't', long)]
391 tag: Vec<String>,
392 /// Minimum level to show (debug < info < warn < error).
393 #[arg(short = 'l', long)]
394 level: Option<String>,
395 /// Follow mode — keep reading new lines as they arrive (tail -f).
396 #[arg(short = 'f', long)]
397 follow: bool,
398 /// Output raw JSON lines instead of logcat-style rendering.
399 #[arg(long)]
400 json: bool,
401 /// List the distinct tags present in the logs (with record counts)
402 /// instead of printing records — handy for discovering `-t` values.
403 #[arg(long)]
404 list_tags: bool,
405 },
406}
407
408pub async fn execute(command: Commands, config: Config) -> Result<()> {
409 match command {
410 Commands::Build {
411 file,
412 path,
413 global,
414 clean,
415 no_update_check,
416 } => run_package::execute_build(config, file, path, global, clean, no_update_check).await,
417 Commands::Start {
418 package,
419 endpoint,
420 config: cfg_file,
421 set,
422 manifest,
423 } => {
424 run_package::execute_start(
425 &config,
426 package.as_deref(),
427 endpoint.as_deref(),
428 cfg_file.as_deref(),
429 &set,
430 manifest.as_deref(),
431 )
432 .await
433 }
434 Commands::Boot {
435 file,
436 log_dir,
437 skip_system,
438 no_update_check,
439 verbose,
440 } => deploy::execute(config, file, log_dir, skip_system, no_update_check, verbose).await,
441 Commands::WatchBoot {
442 state,
443 boot_pid,
444 boot_start_time_ticks,
445 boot_id,
446 } => boot_watchdog::execute(state, boot_pid, boot_start_time_ticks, boot_id).await,
447 Commands::Update { path, file } => update::execute(config, path, file).await,
448 Commands::Shutdown { file } => shutdown::execute(file).await,
449 Commands::Clean {
450 package,
451 file,
452 cache,
453 } => clean::execute(config, package, file, cache).await,
454 Commands::Install { github, path } => install::execute(config, github, path).await,
455 Commands::List => list::execute(config).await,
456 Commands::Info { name } => info::execute(config, &name).await,
457 Commands::Validate { path } => validate::execute(path).await,
458 Commands::Config {
459 set_storage_path,
460 show,
461 } => config::execute(config, set_storage_path, show).await,
462 Commands::Codegen {
463 package,
464 mcp,
465 ros2,
466 clean,
467 out_dir,
468 } => codegen::execute(config, package, mcp, ros2, clean, out_dir).await,
469 Commands::Docs { out_dir } => docs::execute(config, out_dir).await,
470 Commands::Setup { path } => setup::execute(config, path).await,
471 Commands::Path { key } => path::execute(config, key).await,
472 Commands::Caps {
473 server,
474 json,
475 verbose,
476 } => inspect::providers(&server, json, verbose).await,
477 Commands::Contracts {
478 server,
479 prefix,
480 json,
481 verbose,
482 } => inspect::contracts(&server, prefix.as_deref(), json, verbose).await,
483 Commands::Describe {
484 server,
485 provider,
486 json,
487 } => inspect::describe(&server, provider.as_deref(), json).await,
488 Commands::Tools { server, json } => inspect::tools(&server, json).await,
489 Commands::Channels { server } => inspect::channels(&server).await,
490 Commands::Inspect { server } => inspect::inspect(&server).await,
491 Commands::Chat { server } => chat::execute(&server).await,
492 Commands::Init { name, path } => init::execute(&name, path.as_deref()).await,
493 Commands::PackageNew {
494 name,
495 pkg_type,
496 path,
497 } => package_new::execute(&name, &pkg_type, path.as_deref()).await,
498 Commands::Ask {
499 prompt,
500 server,
501 json,
502 } => ask::execute(&server, &prompt, json).await,
503 Commands::Logs {
504 log_dir,
505 tag,
506 level,
507 follow,
508 json,
509 list_tags,
510 } => logs::execute(log_dir, tag, level, follow, json, list_tags).await,
511 }
512}