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;
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}