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