Skip to main content

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}