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