From 932c39bfa7077bdeda1022ed96d573d14cdf2314 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 12 Mar 2026 19:18:22 -0700 Subject: [PATCH 1/3] set up cli reference autogeneration --- .../skills/generate-cli-reference/SKILL.md | 71 +++ .../openshell-cli/cli-reference-generated.md | 447 ++++++++++++++++++ crates/navigator-cli/src/cli_reference.rs | 401 ++++++++++++++++ crates/navigator-cli/src/lib.rs | 1 + crates/navigator-cli/src/main.rs | 30 +- docs/index.md | 1 + docs/reference/cli-generated.md | 447 ++++++++++++++++++ 7 files changed, 1397 insertions(+), 1 deletion(-) create mode 100644 .agents/skills/generate-cli-reference/SKILL.md create mode 100644 .agents/skills/openshell-cli/cli-reference-generated.md create mode 100644 crates/navigator-cli/src/cli_reference.rs create mode 100644 docs/reference/cli-generated.md diff --git a/.agents/skills/generate-cli-reference/SKILL.md b/.agents/skills/generate-cli-reference/SKILL.md new file mode 100644 index 00000000..16d2fa8f --- /dev/null +++ b/.agents/skills/generate-cli-reference/SKILL.md @@ -0,0 +1,71 @@ +--- +name: generate-cli-reference +description: Regenerate the CLI reference markdown from source definitions and deploy it to documentation targets. Use when CLI commands, flags, or descriptions change and the reference docs need updating. Trigger keywords - generate cli reference, regenerate cli docs, update cli reference, cli markdown, cli-reference, docs cli-reference. +--- + +# Generate CLI Reference + +Regenerate the auto-generated CLI reference markdown and deploy it to all documentation targets. + +## When to Use + +Run this after any change to CLI commands, flags, or doc comments in `crates/navigator-cli/src/main.rs`. The generated reference is derived from clap's command tree, so source doc comments are the single source of truth. + +## Prerequisites + +- Rust toolchain available (`cargo` on PATH, or use `mise exec --`) +- The project compiles successfully + +## Steps + +### 1. Build and run the generator + +```bash +export PATH="$HOME/.cargo/bin:$PATH" +cargo run --bin openshell -- docs cli-reference > /tmp/cli-reference-generated.md +``` + +Or with mise: + +```bash +mise exec -- cargo run --bin openshell -- docs cli-reference > /tmp/cli-reference-generated.md +``` + +### 2. Deploy to documentation targets + +Copy the generated file to both locations: + +```bash +cp /tmp/cli-reference-generated.md .agents/skills/openshell-cli/cli-reference-generated.md +cp /tmp/cli-reference-generated.md docs/reference/cli-generated.md +``` + +Note: `.claude/skills/` is a symlink to `.agents/skills/`, so the first copy covers both. + +### 3. Verify + +Spot-check that descriptions end with periods and the command tree looks correct: + +```bash +head -60 .agents/skills/openshell-cli/cli-reference-generated.md +``` + +## Target Files + +| File | Purpose | +|------|---------| +| `.agents/skills/openshell-cli/cli-reference-generated.md` | Agent skill reference (also serves `.claude/skills/` via symlink) | +| `docs/reference/cli-generated.md` | User-facing documentation site | + +## Key Source Files + +| File | Purpose | +|------|---------| +| `crates/navigator-cli/src/main.rs` | CLI definitions (commands, flags, doc comments) | +| `crates/navigator-cli/src/cli_reference.rs` | Markdown generator logic | + +## Notes + +- The generator is a hidden subcommand (`openshell docs cli-reference`) that introspects clap's command tree at runtime. +- Clap strips trailing periods from single-line doc comments; the generator restores them via `ensure_period()`. +- The generated files contain a `` header. Always regenerate rather than hand-editing. diff --git a/.agents/skills/openshell-cli/cli-reference-generated.md b/.agents/skills/openshell-cli/cli-reference-generated.md new file mode 100644 index 00000000..a000f038 --- /dev/null +++ b/.agents/skills/openshell-cli/cli-reference-generated.md @@ -0,0 +1,447 @@ +# OpenShell CLI Reference + + + + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `OPENSHELL_GATEWAY` | Gateway name to operate on (resolved from stored metadata). | +| `OPENSHELL_GATEWAY_ENDPOINT` | Gateway endpoint URL (e.g. ). Connects directly without looking up gateway metadata. | + +## Command Tree + +```text +openshell +├── sandbox +│ ├── create [COMMAND] +│ ├── get [NAME] +│ ├── list +│ ├── delete [NAME] +│ ├── connect [NAME] +│ ├── upload [DEST] +│ ├── download [DEST] +│ └── ssh-config [NAME] +├── forward +│ ├── start [NAME] +│ ├── stop [NAME] +│ └── list +├── logs [NAME] +├── policy +│ ├── set [NAME] +│ ├── get [NAME] +│ └── list [NAME] +├── provider +│ ├── create +│ ├── get +│ ├── list +│ ├── update +│ └── delete +├── gateway +│ ├── start +│ ├── stop +│ ├── destroy +│ ├── add +│ ├── login [NAME] +│ ├── select [NAME] +│ └── info +├── status +├── inference +│ ├── set +│ ├── update +│ └── get +├── term +└── completions +``` + +--- + +## Sandbox Commands + +Manage sandboxes. + + +### `openshell sandbox create [COMMAND]` + +Create a sandbox. + +| Flag | Description | +|------|-------------| +| `--name ` | Optional sandbox name (auto-generated when omitted). | +| `--from ` | Sandbox source: a community sandbox name (e.g., `openclaw`), a path to a Dockerfile or directory containing one, or a full container image reference (e.g., `myregistry.com/img:tag`). | +| `--upload ` | Upload local files into the sandbox before running. | +| `--no-git-ignore` | Disable `.gitignore` filtering for `--upload`. | +| `--no-keep` | Delete the sandbox after the initial command or shell exits. | +| `--editor ` | Launch a remote editor after the sandbox is ready. Keeps the sandbox alive and installs OpenShell-managed SSH config. | +| `--remote ` | SSH destination for remote bootstrap (e.g., user@hostname). Only used when no cluster exists yet; ignored if a cluster is already active. | +| `--ssh-key ` | Path to SSH private key for remote bootstrap. | +| `--provider ` | Provider names to attach to this sandbox. | +| `--policy ` | Path to a custom sandbox policy YAML file. Overrides the built-in default and the `OPENSHELL_SANDBOX_POLICY` env var. | +| `--forward ` | Forward a local port to the sandbox before the initial command or shell starts. Keeps the sandbox alive. | +| `--tty` | Allocate a pseudo-terminal for the remote command. Defaults to auto-detection (on when stdin and stdout are terminals). Use --tty to force a PTY even when auto-detection fails, or --no-tty to disable. | +| `--no-tty` | Disable pseudo-terminal allocation. | +| `--no-bootstrap` | Never bootstrap a gateway automatically; error if none is available. | +| `--auto-providers` | Auto-create missing providers from local credentials. | +| `--no-auto-providers` | Never auto-create providers; error if required providers are missing. | +| `[COMMAND]` | Command to run after "--" (defaults to an interactive shell). | + +### `openshell sandbox get [NAME]` + +Fetch a sandbox by name. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | + +### `openshell sandbox list` + +List sandboxes. + +| Flag | Default | Description | +|------|---------|-------------| +| `--limit ` | `100` | Maximum number of sandboxes to return. | +| `--offset ` | `0` | Offset into the sandbox list. | +| `--ids` | | Print only sandbox ids (one per line). | +| `--names` | | Print only sandbox names (one per line). | + +### `openshell sandbox delete [NAME]` + +Delete a sandbox by name. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox names. | +| `--all` | Delete all sandboxes. | + +### `openshell sandbox connect [NAME]` + +Connect to a sandbox. + +When no name is given, reconnects to the last-used sandbox. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | +| `--editor ` | Launch a remote editor instead of an interactive shell. Installs OpenShell-managed SSH config if needed. | + +### `openshell sandbox upload [DEST]` + +Upload local files to a sandbox. + +| Flag | Description | +|------|-------------| +| `` | Sandbox name. | +| `` | Local path to upload. | +| `[DEST]` | Destination path in the sandbox (defaults to `/sandbox`). | +| `--no-git-ignore` | Disable `.gitignore` filtering (uploads everything). | + +### `openshell sandbox download [DEST]` + +Download files from a sandbox. + +| Flag | Description | +|------|-------------| +| `` | Sandbox name. | +| `` | Sandbox path to download. | +| `[DEST]` | Local destination (defaults to `.`). | + +### `openshell sandbox ssh-config [NAME]` + +Print an SSH config entry for a sandbox. + +Outputs a Host block suitable for appending to ~/.ssh/config, enabling tools like `VSCode` Remote-SSH to connect to the sandbox. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | + +--- + +## Forward Commands + +Manage port forwarding to a sandbox. + + +### `openshell forward start [NAME]` + +Start forwarding a local port to a sandbox. + +| Flag | Description | +|------|-------------| +| `` | Port to forward (used as both local and remote port). | +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | +| `-d`, `--background` | Run the forward in the background and exit immediately. | + +### `openshell forward stop [NAME]` + +Stop a background port forward. + +| Flag | Description | +|------|-------------| +| `` | Port that was forwarded. | +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | + +### `openshell forward list` + +List active port forwards. + + +--- + +## Policy Commands + +Manage sandbox policy. + + +### `openshell policy set [NAME]` + +Update policy on a live sandbox. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `--policy ` | | Path to the policy YAML file. | +| `--wait` | | Wait for the sandbox to load the policy. | +| `--timeout ` | `60` | Timeout for --wait in seconds. | + +### `openshell policy get [NAME]` + +Show current active policy for a sandbox. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `--rev ` | `0` | Show a specific policy revision (default: latest). | +| `--full` | | Print the full policy as YAML. | + +### `openshell policy list [NAME]` + +List policy history for a sandbox. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `--limit ` | `20` | Maximum number of revisions to return. | + +--- + +## Provider Commands + +Manage provider configuration. + + +### `openshell provider create` + +Create a provider config. + +| Flag | Description | +|------|-------------| +| `--name ` | Provider name. | +| `--type ` | Provider type. | +| `--from-existing` | Load provider credentials/config from existing local state. | +| `--credential ` | Provider credential pair (`KEY=VALUE`) or env lookup key (`KEY`). | +| `--config ` | Provider config key/value pair. | + +### `openshell provider get ` + +Fetch a provider by name. + +| Flag | Description | +|------|-------------| +| `` | Provider name. | + +### `openshell provider list` + +List providers. + +| Flag | Default | Description | +|------|---------|-------------| +| `--limit ` | `100` | Maximum number of providers to return. | +| `--offset ` | `0` | Offset into the provider list. | +| `--names` | | Print only provider names, one per line. | + +### `openshell provider update ` + +Update an existing provider's credentials or config. + +| Flag | Description | +|------|-------------| +| `` | Provider name. | +| `--from-existing` | Re-discover credentials from existing local state (e.g. env vars, config files). | +| `--credential ` | Provider credential pair (`KEY=VALUE`) or env lookup key (`KEY`). | +| `--config ` | Provider config key/value pair. | + +### `openshell provider delete ` + +Delete providers by name. + +| Flag | Description | +|------|-------------| +| `` | Provider names. | + +--- + +## Gateway Commands + +Manage the gateway lifecycle. + + +### `openshell gateway start` + +Deploy/start the gateway. + +| Flag | Default | Description | +|------|---------|-------------| +| `--name ` | `openshell` | Gateway name. Env: `OPENSHELL_GATEWAY`. | +| `--remote ` | | SSH destination for remote deployment (e.g., user@hostname). | +| `--ssh-key ` | | Path to SSH private key for remote deployment. | +| `--port ` | `8080` | Host port to map to the gateway (default: 8080). | +| `--gateway-host ` | | Override the gateway host written into cluster metadata. | +| `--recreate` | | Destroy and recreate the gateway from scratch if one already exists. | +| `--plaintext` | | Listen on plaintext HTTP instead of mTLS. | +| `--disable-gateway-auth` | | Disable gateway authentication (mTLS client certificate requirement). | +| `--registry-token ` | | Authentication token for pulling container images from ghcr.io. Env: `OPENSHELL_REGISTRY_TOKEN`. | +| `--gpu` | | Enable NVIDIA GPU passthrough. | + +### `openshell gateway stop` + +Stop the gateway (preserves state). + +| Flag | Description | +|------|-------------| +| `--name ` | Gateway name (defaults to active gateway). Env: `OPENSHELL_GATEWAY`. | +| `--remote ` | Override SSH destination (auto-resolved from gateway metadata). | +| `--ssh-key ` | Path to SSH private key for remote gateway. | + +### `openshell gateway destroy` + +Destroy the gateway and its state. + +| Flag | Description | +|------|-------------| +| `--name ` | Gateway name (defaults to active gateway). Env: `OPENSHELL_GATEWAY`. | +| `--remote ` | Override SSH destination (auto-resolved from gateway metadata). | +| `--ssh-key ` | Path to SSH private key for remote gateway. | + +### `openshell gateway add ` + +Add an existing gateway. + +Registers a gateway endpoint so it appears in `openshell gateway select`. + +Without extra flags the gateway is treated as an edge-authenticated (cloud) gateway and a browser is opened for authentication. + +Pass `--remote ` to register a remote mTLS gateway whose Docker daemon is reachable over SSH. Pass `--local` to register a local mTLS gateway running in Docker on this machine. In both cases the CLI extracts mTLS certificates from the running container automatically. + +An `ssh://` endpoint (e.g., `ssh://user@host:8080`) is shorthand for `--remote user@host` with the endpoint derived from the URL. + +| Flag | Description | +|------|-------------| +| `` | Gateway endpoint URL (e.g., `https://10.0.0.5:8080` or `ssh://user@host:8080`). | +| `--name ` | Gateway name (auto-derived from the endpoint hostname when omitted). | +| `--remote ` | Register a remote mTLS gateway accessible via SSH. | +| `--ssh-key ` | SSH private key for the remote host (used with `--remote` or `ssh://`). | +| `--local` | Register a local mTLS gateway running in Docker on this machine. | + +### `openshell gateway login [NAME]` + +Authenticate with an edge-authenticated gateway. + +Opens a browser for the edge proxy's login flow and stores the token locally. Use this to re-authenticate when a token expires. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Gateway name (defaults to the active gateway). | + +### `openshell gateway select [NAME]` + +Select the active gateway. + +When called without a name, opens an interactive chooser on a TTY and lists available gateways in non-interactive mode. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Gateway name (omit to choose interactively or list in non-interactive mode). | + +### `openshell gateway info` + +Show gateway deployment details. + +| Flag | Description | +|------|-------------| +| `--name ` | Gateway name (defaults to active gateway). Env: `OPENSHELL_GATEWAY`. | + +--- + +## Inference Commands + +Manage inference configuration. + + +### `openshell inference set` + +Set gateway-level inference provider and model. + +| Flag | Description | +|------|-------------| +| `--provider ` | Provider name. | +| `--model ` | Model identifier to force for generation calls. | +| `--system` | Configure the system inference route instead of the user-facing route. System inference is used by platform functions (e.g. the agent harness) and is not accessible to user code. | + +### `openshell inference update` + +Update gateway-level inference configuration (partial update). + +| Flag | Description | +|------|-------------| +| `--provider ` | Provider name (unchanged if omitted). | +| `--model ` | Model identifier (unchanged if omitted). | +| `--system` | Target the system inference route. | + +### `openshell inference get` + +Get gateway-level inference provider and model. + +| Flag | Description | +|------|-------------| +| `--system` | Show the system inference route instead of the user-facing route. When omitted, both routes are displayed. | + +--- + +## Additional Commands + + +### `openshell logs [NAME]` + +View sandbox logs. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `-n ` | `200` | Number of log lines to return. | +| `--tail` | | Stream live logs. | +| `--since ` | | Only show logs from this duration ago (e.g. 5m, 1h, 30s). | +| `--source ` | `all` | Filter by log source: "gateway", "sandbox", or "all" (default). Can be specified multiple times: --source gateway --source sandbox. | +| `--level ` | | Minimum log level to display: error, warn, info (default), debug, trace. | + +### `openshell status` + +Show gateway status and information. + + +### `openshell term` + +Launch the `OpenShell` interactive TUI. + +| Flag | Default | Description | +|------|---------|-------------| +| `--theme ` | `auto` | Color theme for the TUI: auto, dark, or light. Env: `OPENSHELL_THEME`. | + +### `openshell completions ` + +Generate shell completions. + +| Flag | Description | +|------|-------------| +| `` | Shell to generate completions for. | diff --git a/crates/navigator-cli/src/cli_reference.rs b/crates/navigator-cli/src/cli_reference.rs new file mode 100644 index 00000000..06e81025 --- /dev/null +++ b/crates/navigator-cli/src/cli_reference.rs @@ -0,0 +1,401 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Auto-generate a Markdown CLI reference from a clap [`Command`] tree. +//! +//! Usage from the CLI: `openshell docs cli-reference` + +use clap::{Arg, Command}; +use std::fmt::Write; + +/// Generate a complete CLI reference document from a clap [`Command`] tree. +pub fn generate(cmd: &Command) -> String { + let mut out = String::with_capacity(16_384); + + writeln!( + out, + "# OpenShell CLI Reference\n\ + \n\ + \n\ + " + ) + .unwrap(); + + write_env_vars(cmd, &mut out); + write_command_tree(cmd, &mut out); + write_all_commands(cmd, &mut out, cmd.get_name()); + + out +} + +// --------------------------------------------------------------------------- +// Environment variables +// --------------------------------------------------------------------------- + +fn write_env_vars(cmd: &Command, out: &mut String) { + let vars: Vec<_> = cmd + .get_arguments() + .filter(|a| a.get_env().is_some() && !a.is_hide_set()) + .collect(); + + if vars.is_empty() { + return; + } + + writeln!(out, "\n## Environment Variables\n").unwrap(); + writeln!(out, "| Variable | Description |").unwrap(); + writeln!(out, "|----------|-------------|").unwrap(); + + for arg in vars { + let env = arg.get_env().unwrap().to_string_lossy(); + let help = arg_help(arg); + writeln!(out, "| `{env}` | {help} |").unwrap(); + } +} + +// --------------------------------------------------------------------------- +// Command tree +// --------------------------------------------------------------------------- + +fn write_command_tree(root: &Command, out: &mut String) { + writeln!(out, "\n## Command Tree\n").unwrap(); + writeln!(out, "```text").unwrap(); + writeln!(out, "{}", root.get_name()).unwrap(); + + let visible: Vec<_> = root + .get_subcommands() + .filter(|c| !c.is_hide_set()) + .collect(); + + for (i, sub) in visible.iter().enumerate() { + let is_last = i == visible.len() - 1; + write_tree_node(sub, out, "", is_last); + } + + writeln!(out, "```").unwrap(); +} + +fn write_tree_node(cmd: &Command, out: &mut String, prefix: &str, is_last: bool) { + let connector = if is_last { "└── " } else { "├── " }; + let child_prefix = if is_last { + format!("{prefix} ") + } else { + format!("{prefix}│ ") + }; + + let positionals = positional_synopsis(cmd); + let name = cmd.get_name(); + + writeln!(out, "{prefix}{connector}{name}{positionals}").unwrap(); + + let children: Vec<_> = cmd + .get_subcommands() + .filter(|c| !c.is_hide_set()) + .collect(); + + for (i, child) in children.iter().enumerate() { + write_tree_node(child, out, &child_prefix, i == children.len() - 1); + } +} + +/// Build a compact positional-args synopsis like ` [dest]` or ` [-- CMD...]`. +fn positional_synopsis(cmd: &Command) -> String { + let mut parts = String::new(); + + for arg in cmd.get_arguments() { + if arg.is_hide_set() || !arg.is_positional() { + continue; + } + let id = arg.get_id().as_str(); + if id == "help" || id == "version" { + continue; + } + + let value = arg + .get_value_names() + .and_then(|v| v.first().map(|s| s.as_str())) + .unwrap_or(id); + + if arg.is_last_set() { + write!(parts, " [-- {value}...]").unwrap(); + } else if arg.is_required_set() { + write!(parts, " <{value}>").unwrap(); + } else { + write!(parts, " [{value}]").unwrap(); + } + } + + parts +} + +// --------------------------------------------------------------------------- +// Detailed command sections +// --------------------------------------------------------------------------- + +fn write_all_commands(root: &Command, out: &mut String, root_name: &str) { + let mut groups: Vec<&Command> = Vec::new(); + let mut leaves: Vec<&Command> = Vec::new(); + + for sub in root.get_subcommands() { + if sub.is_hide_set() { + continue; + } + let has_children = sub.get_subcommands().any(|c| !c.is_hide_set()); + if has_children { + groups.push(sub); + } else { + leaves.push(sub); + } + } + + for group in &groups { + let title = titlecase(group.get_name()); + writeln!(out, "\n---\n").unwrap(); + writeln!(out, "## {title} Commands\n").unwrap(); + + if let Some(about) = group.get_about() { + writeln!(out, "{}\n", ensure_period(&about.to_string())).unwrap(); + } + + if let Some(aliases) = visible_aliases_str(group) { + writeln!(out, "**Alias:** `{aliases}`\n").unwrap(); + } + + let children: Vec<_> = group + .get_subcommands() + .filter(|c| !c.is_hide_set()) + .collect(); + + for child in &children { + write_leaf_command( + child, + out, + &format!("{root_name} {}", group.get_name()), + ); + } + } + + if !leaves.is_empty() { + writeln!(out, "\n---\n").unwrap(); + writeln!(out, "## Additional Commands\n").unwrap(); + + for leaf in &leaves { + write_leaf_command(leaf, out, root_name); + } + } +} + +fn write_leaf_command(cmd: &Command, out: &mut String, parent_path: &str) { + let positionals = positional_synopsis(cmd); + let full_cmd = format!("{parent_path} {}{positionals}", cmd.get_name()); + + writeln!(out, "\n### `{full_cmd}`\n").unwrap(); + + if let Some(about) = cmd.get_long_about().or_else(|| cmd.get_about()) { + let text = ensure_period(&strip_ansi(&about.to_string())); + writeln!(out, "{text}\n").unwrap(); + } + + if let Some(aliases) = visible_aliases_str(cmd) { + writeln!(out, "**Alias:** `{aliases}`\n").unwrap(); + } + + let flags: Vec<_> = cmd + .get_arguments() + .filter(|a| !a.is_hide_set() && !a.is_global_set()) + .filter(|a| { + let id = a.get_id().as_str(); + id != "help" && id != "version" + }) + .collect(); + + if flags.is_empty() { + return; + } + + let has_defaults = flags.iter().any(|a| !a.get_default_values().is_empty()); + + if has_defaults { + writeln!(out, "| Flag | Default | Description |").unwrap(); + writeln!(out, "|------|---------|-------------|").unwrap(); + } else { + writeln!(out, "| Flag | Description |").unwrap(); + writeln!(out, "|------|-------------|").unwrap(); + } + + for arg in &flags { + let flag_col = format_flag_name(arg); + let help = arg_help(arg); + + let env_suffix = match arg.get_env() { + Some(env) => format!(" Env: `{}`.", env.to_string_lossy()), + None => String::new(), + }; + + if has_defaults { + let default = format_default(arg); + writeln!(out, "| {flag_col} | {default} | {help}{env_suffix} |").unwrap(); + } else { + writeln!(out, "| {flag_col} | {help}{env_suffix} |").unwrap(); + } + } +} + +// --------------------------------------------------------------------------- +// Formatting helpers +// --------------------------------------------------------------------------- + +fn format_flag_name(arg: &Arg) -> String { + let value_name = arg + .get_value_names() + .and_then(|v| v.first().map(|s| s.as_str())); + + if arg.is_positional() { + let name = value_name.unwrap_or(arg.get_id().as_str()); + if arg.is_last_set() { + return format!("`[-- {name}...]`"); + } + if arg.is_required_set() { + return format!("`<{name}>`"); + } + return format!("`[{name}]`"); + } + + let mut parts = Vec::new(); + if let Some(short) = arg.get_short() { + parts.push(format!("-{short}")); + } + if let Some(long) = arg.get_long() { + parts.push(format!("--{long}")); + } + + let name = parts.join("`, `"); + + match value_name { + Some(v) if !is_bool_flag(arg) => format!("`{name} <{v}>`"), + _ => format!("`{name}`"), + } +} + +fn format_default(arg: &Arg) -> String { + let vals = arg.get_default_values(); + if vals.is_empty() { + return String::new(); + } + let joined: String = vals + .iter() + .map(|v| v.to_string_lossy().to_string()) + .collect::>() + .join(", "); + if joined.is_empty() { + return String::new(); + } + format!("`{joined}`") +} + +fn arg_help(arg: &Arg) -> String { + arg.get_help() + .map(|h| ensure_period(&strip_ansi(&h.to_string()))) + .unwrap_or_default() +} + +fn visible_aliases_str(cmd: &Command) -> Option { + let aliases: Vec<_> = cmd.get_visible_aliases().collect(); + if aliases.is_empty() { + return None; + } + Some(aliases.join("`, `")) +} + +fn is_bool_flag(arg: &Arg) -> bool { + arg.get_num_args().is_some_and(|r| r.max_values() == 0) + || matches!( + arg.get_action(), + clap::ArgAction::SetTrue | clap::ArgAction::SetFalse + ) +} + +fn titlecase(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + None => String::new(), + Some(c) => c.to_uppercase().collect::() + chars.as_str(), + } +} + +/// Clap's derive macro strips trailing periods from single-line doc comments. +/// This restores them so generated documentation has consistent punctuation. +fn ensure_period(s: &str) -> String { + let trimmed = s.trim_end(); + if trimmed.is_empty() { + return String::new(); + } + if trimmed.ends_with('.') || trimmed.ends_with('!') || trimmed.ends_with('?') { + trimmed.to_string() + } else { + format!("{trimmed}.") + } +} + +fn strip_ansi(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut in_escape = false; + for c in s.chars() { + if c == '\x1b' { + in_escape = true; + continue; + } + if in_escape { + if c.is_ascii_alphabetic() { + in_escape = false; + } + continue; + } + result.push(c); + } + result +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::{CommandFactory, Parser}; + + #[derive(Parser, Debug)] + #[command(name = "test-cli")] + struct TestCli { + #[command(subcommand)] + command: Option, + } + + #[derive(clap::Subcommand, Debug)] + enum TestCommands { + /// Do something. + Hello { + /// Your name. + #[arg(long)] + name: String, + + /// Greeting count. + #[arg(short, default_value_t = 1)] + n: u32, + }, + } + + #[test] + fn generates_markdown_with_command_tree() { + let cmd = TestCli::command(); + let md = generate(&cmd); + + assert!(md.contains("# OpenShell CLI Reference"), "has title"); + assert!(md.contains("## Command Tree"), "has tree section"); + assert!(md.contains("hello"), "has hello command in tree"); + assert!(md.contains("`--name `"), "has flag"); + } + + #[test] + fn strip_ansi_removes_escape_codes() { + assert_eq!(strip_ansi("\x1b[1mBOLD\x1b[0m"), "BOLD"); + assert_eq!(strip_ansi("no codes"), "no codes"); + } +} diff --git a/crates/navigator-cli/src/lib.rs b/crates/navigator-cli/src/lib.rs index 09e05449..3d713833 100644 --- a/crates/navigator-cli/src/lib.rs +++ b/crates/navigator-cli/src/lib.rs @@ -10,6 +10,7 @@ pub(crate) static TEST_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(() pub mod auth; pub mod bootstrap; +pub mod cli_reference; pub mod completers; pub mod edge_tunnel; pub mod run; diff --git a/crates/navigator-cli/src/main.rs b/crates/navigator-cli/src/main.rs index 88d87049..7c5189b4 100644 --- a/crates/navigator-cli/src/main.rs +++ b/crates/navigator-cli/src/main.rs @@ -380,7 +380,7 @@ enum Commands { since: Option, /// Filter by log source: "gateway", "sandbox", or "all" (default). - /// Can be specified multiple times: --source gateway --source sandbox + /// Can be specified multiple times: --source gateway --source sandbox. #[arg(long, default_value = "all")] source: Vec, @@ -463,6 +463,13 @@ enum Commands { shell: CompletionShell, }, + /// Generate documentation from CLI source definitions. + #[command(hide = true, help_template = SUBCOMMAND_HELP_TEMPLATE)] + Docs { + #[command(subcommand)] + command: Option, + }, + /// SSH proxy (used by `ProxyCommand`). /// /// Two mutually exclusive modes: @@ -1002,6 +1009,13 @@ enum DoctorCommands { LlmTxt, } +#[derive(Subcommand, Debug)] +enum DocsCommands { + /// Print the CLI reference as Markdown. + #[command(help_template = LEAF_HELP_TEMPLATE)] + CliReference, +} + #[derive(Subcommand, Debug)] enum SandboxCommands { /// Create a sandbox. @@ -2080,6 +2094,20 @@ async fn main() -> Result<()> { .write_all(script.as_bytes()) .map_err(|e| miette::miette!("failed to write completions: {e}"))?; } + Some(Commands::Docs { + command: Some(DocsCommands::CliReference), + }) => { + let md = navigator_cli::cli_reference::generate(&Cli::command()); + print!("{md}"); + } + Some(Commands::Docs { command: None }) => { + Cli::command() + .find_subcommand_mut("docs") + .expect("docs subcommand exists") + .print_help() + .expect("Failed to print help"); + } + Some(Commands::SshProxy { gateway, sandbox_id, diff --git a/docs/index.md b/docs/index.md index 312c32ad..26598ba3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -223,6 +223,7 @@ inference/configure :hidden: reference/cli +reference/cli-generated reference/default-policy reference/policy-schema reference/support-matrix diff --git a/docs/reference/cli-generated.md b/docs/reference/cli-generated.md new file mode 100644 index 00000000..a000f038 --- /dev/null +++ b/docs/reference/cli-generated.md @@ -0,0 +1,447 @@ +# OpenShell CLI Reference + + + + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `OPENSHELL_GATEWAY` | Gateway name to operate on (resolved from stored metadata). | +| `OPENSHELL_GATEWAY_ENDPOINT` | Gateway endpoint URL (e.g. ). Connects directly without looking up gateway metadata. | + +## Command Tree + +```text +openshell +├── sandbox +│ ├── create [COMMAND] +│ ├── get [NAME] +│ ├── list +│ ├── delete [NAME] +│ ├── connect [NAME] +│ ├── upload [DEST] +│ ├── download [DEST] +│ └── ssh-config [NAME] +├── forward +│ ├── start [NAME] +│ ├── stop [NAME] +│ └── list +├── logs [NAME] +├── policy +│ ├── set [NAME] +│ ├── get [NAME] +│ └── list [NAME] +├── provider +│ ├── create +│ ├── get +│ ├── list +│ ├── update +│ └── delete +├── gateway +│ ├── start +│ ├── stop +│ ├── destroy +│ ├── add +│ ├── login [NAME] +│ ├── select [NAME] +│ └── info +├── status +├── inference +│ ├── set +│ ├── update +│ └── get +├── term +└── completions +``` + +--- + +## Sandbox Commands + +Manage sandboxes. + + +### `openshell sandbox create [COMMAND]` + +Create a sandbox. + +| Flag | Description | +|------|-------------| +| `--name ` | Optional sandbox name (auto-generated when omitted). | +| `--from ` | Sandbox source: a community sandbox name (e.g., `openclaw`), a path to a Dockerfile or directory containing one, or a full container image reference (e.g., `myregistry.com/img:tag`). | +| `--upload ` | Upload local files into the sandbox before running. | +| `--no-git-ignore` | Disable `.gitignore` filtering for `--upload`. | +| `--no-keep` | Delete the sandbox after the initial command or shell exits. | +| `--editor ` | Launch a remote editor after the sandbox is ready. Keeps the sandbox alive and installs OpenShell-managed SSH config. | +| `--remote ` | SSH destination for remote bootstrap (e.g., user@hostname). Only used when no cluster exists yet; ignored if a cluster is already active. | +| `--ssh-key ` | Path to SSH private key for remote bootstrap. | +| `--provider ` | Provider names to attach to this sandbox. | +| `--policy ` | Path to a custom sandbox policy YAML file. Overrides the built-in default and the `OPENSHELL_SANDBOX_POLICY` env var. | +| `--forward ` | Forward a local port to the sandbox before the initial command or shell starts. Keeps the sandbox alive. | +| `--tty` | Allocate a pseudo-terminal for the remote command. Defaults to auto-detection (on when stdin and stdout are terminals). Use --tty to force a PTY even when auto-detection fails, or --no-tty to disable. | +| `--no-tty` | Disable pseudo-terminal allocation. | +| `--no-bootstrap` | Never bootstrap a gateway automatically; error if none is available. | +| `--auto-providers` | Auto-create missing providers from local credentials. | +| `--no-auto-providers` | Never auto-create providers; error if required providers are missing. | +| `[COMMAND]` | Command to run after "--" (defaults to an interactive shell). | + +### `openshell sandbox get [NAME]` + +Fetch a sandbox by name. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | + +### `openshell sandbox list` + +List sandboxes. + +| Flag | Default | Description | +|------|---------|-------------| +| `--limit ` | `100` | Maximum number of sandboxes to return. | +| `--offset ` | `0` | Offset into the sandbox list. | +| `--ids` | | Print only sandbox ids (one per line). | +| `--names` | | Print only sandbox names (one per line). | + +### `openshell sandbox delete [NAME]` + +Delete a sandbox by name. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox names. | +| `--all` | Delete all sandboxes. | + +### `openshell sandbox connect [NAME]` + +Connect to a sandbox. + +When no name is given, reconnects to the last-used sandbox. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | +| `--editor ` | Launch a remote editor instead of an interactive shell. Installs OpenShell-managed SSH config if needed. | + +### `openshell sandbox upload [DEST]` + +Upload local files to a sandbox. + +| Flag | Description | +|------|-------------| +| `` | Sandbox name. | +| `` | Local path to upload. | +| `[DEST]` | Destination path in the sandbox (defaults to `/sandbox`). | +| `--no-git-ignore` | Disable `.gitignore` filtering (uploads everything). | + +### `openshell sandbox download [DEST]` + +Download files from a sandbox. + +| Flag | Description | +|------|-------------| +| `` | Sandbox name. | +| `` | Sandbox path to download. | +| `[DEST]` | Local destination (defaults to `.`). | + +### `openshell sandbox ssh-config [NAME]` + +Print an SSH config entry for a sandbox. + +Outputs a Host block suitable for appending to ~/.ssh/config, enabling tools like `VSCode` Remote-SSH to connect to the sandbox. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | + +--- + +## Forward Commands + +Manage port forwarding to a sandbox. + + +### `openshell forward start [NAME]` + +Start forwarding a local port to a sandbox. + +| Flag | Description | +|------|-------------| +| `` | Port to forward (used as both local and remote port). | +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | +| `-d`, `--background` | Run the forward in the background and exit immediately. | + +### `openshell forward stop [NAME]` + +Stop a background port forward. + +| Flag | Description | +|------|-------------| +| `` | Port that was forwarded. | +| `[NAME]` | Sandbox name (defaults to last-used sandbox). | + +### `openshell forward list` + +List active port forwards. + + +--- + +## Policy Commands + +Manage sandbox policy. + + +### `openshell policy set [NAME]` + +Update policy on a live sandbox. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `--policy ` | | Path to the policy YAML file. | +| `--wait` | | Wait for the sandbox to load the policy. | +| `--timeout ` | `60` | Timeout for --wait in seconds. | + +### `openshell policy get [NAME]` + +Show current active policy for a sandbox. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `--rev ` | `0` | Show a specific policy revision (default: latest). | +| `--full` | | Print the full policy as YAML. | + +### `openshell policy list [NAME]` + +List policy history for a sandbox. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `--limit ` | `20` | Maximum number of revisions to return. | + +--- + +## Provider Commands + +Manage provider configuration. + + +### `openshell provider create` + +Create a provider config. + +| Flag | Description | +|------|-------------| +| `--name ` | Provider name. | +| `--type ` | Provider type. | +| `--from-existing` | Load provider credentials/config from existing local state. | +| `--credential ` | Provider credential pair (`KEY=VALUE`) or env lookup key (`KEY`). | +| `--config ` | Provider config key/value pair. | + +### `openshell provider get ` + +Fetch a provider by name. + +| Flag | Description | +|------|-------------| +| `` | Provider name. | + +### `openshell provider list` + +List providers. + +| Flag | Default | Description | +|------|---------|-------------| +| `--limit ` | `100` | Maximum number of providers to return. | +| `--offset ` | `0` | Offset into the provider list. | +| `--names` | | Print only provider names, one per line. | + +### `openshell provider update ` + +Update an existing provider's credentials or config. + +| Flag | Description | +|------|-------------| +| `` | Provider name. | +| `--from-existing` | Re-discover credentials from existing local state (e.g. env vars, config files). | +| `--credential ` | Provider credential pair (`KEY=VALUE`) or env lookup key (`KEY`). | +| `--config ` | Provider config key/value pair. | + +### `openshell provider delete ` + +Delete providers by name. + +| Flag | Description | +|------|-------------| +| `` | Provider names. | + +--- + +## Gateway Commands + +Manage the gateway lifecycle. + + +### `openshell gateway start` + +Deploy/start the gateway. + +| Flag | Default | Description | +|------|---------|-------------| +| `--name ` | `openshell` | Gateway name. Env: `OPENSHELL_GATEWAY`. | +| `--remote ` | | SSH destination for remote deployment (e.g., user@hostname). | +| `--ssh-key ` | | Path to SSH private key for remote deployment. | +| `--port ` | `8080` | Host port to map to the gateway (default: 8080). | +| `--gateway-host ` | | Override the gateway host written into cluster metadata. | +| `--recreate` | | Destroy and recreate the gateway from scratch if one already exists. | +| `--plaintext` | | Listen on plaintext HTTP instead of mTLS. | +| `--disable-gateway-auth` | | Disable gateway authentication (mTLS client certificate requirement). | +| `--registry-token ` | | Authentication token for pulling container images from ghcr.io. Env: `OPENSHELL_REGISTRY_TOKEN`. | +| `--gpu` | | Enable NVIDIA GPU passthrough. | + +### `openshell gateway stop` + +Stop the gateway (preserves state). + +| Flag | Description | +|------|-------------| +| `--name ` | Gateway name (defaults to active gateway). Env: `OPENSHELL_GATEWAY`. | +| `--remote ` | Override SSH destination (auto-resolved from gateway metadata). | +| `--ssh-key ` | Path to SSH private key for remote gateway. | + +### `openshell gateway destroy` + +Destroy the gateway and its state. + +| Flag | Description | +|------|-------------| +| `--name ` | Gateway name (defaults to active gateway). Env: `OPENSHELL_GATEWAY`. | +| `--remote ` | Override SSH destination (auto-resolved from gateway metadata). | +| `--ssh-key ` | Path to SSH private key for remote gateway. | + +### `openshell gateway add ` + +Add an existing gateway. + +Registers a gateway endpoint so it appears in `openshell gateway select`. + +Without extra flags the gateway is treated as an edge-authenticated (cloud) gateway and a browser is opened for authentication. + +Pass `--remote ` to register a remote mTLS gateway whose Docker daemon is reachable over SSH. Pass `--local` to register a local mTLS gateway running in Docker on this machine. In both cases the CLI extracts mTLS certificates from the running container automatically. + +An `ssh://` endpoint (e.g., `ssh://user@host:8080`) is shorthand for `--remote user@host` with the endpoint derived from the URL. + +| Flag | Description | +|------|-------------| +| `` | Gateway endpoint URL (e.g., `https://10.0.0.5:8080` or `ssh://user@host:8080`). | +| `--name ` | Gateway name (auto-derived from the endpoint hostname when omitted). | +| `--remote ` | Register a remote mTLS gateway accessible via SSH. | +| `--ssh-key ` | SSH private key for the remote host (used with `--remote` or `ssh://`). | +| `--local` | Register a local mTLS gateway running in Docker on this machine. | + +### `openshell gateway login [NAME]` + +Authenticate with an edge-authenticated gateway. + +Opens a browser for the edge proxy's login flow and stores the token locally. Use this to re-authenticate when a token expires. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Gateway name (defaults to the active gateway). | + +### `openshell gateway select [NAME]` + +Select the active gateway. + +When called without a name, opens an interactive chooser on a TTY and lists available gateways in non-interactive mode. + +| Flag | Description | +|------|-------------| +| `[NAME]` | Gateway name (omit to choose interactively or list in non-interactive mode). | + +### `openshell gateway info` + +Show gateway deployment details. + +| Flag | Description | +|------|-------------| +| `--name ` | Gateway name (defaults to active gateway). Env: `OPENSHELL_GATEWAY`. | + +--- + +## Inference Commands + +Manage inference configuration. + + +### `openshell inference set` + +Set gateway-level inference provider and model. + +| Flag | Description | +|------|-------------| +| `--provider ` | Provider name. | +| `--model ` | Model identifier to force for generation calls. | +| `--system` | Configure the system inference route instead of the user-facing route. System inference is used by platform functions (e.g. the agent harness) and is not accessible to user code. | + +### `openshell inference update` + +Update gateway-level inference configuration (partial update). + +| Flag | Description | +|------|-------------| +| `--provider ` | Provider name (unchanged if omitted). | +| `--model ` | Model identifier (unchanged if omitted). | +| `--system` | Target the system inference route. | + +### `openshell inference get` + +Get gateway-level inference provider and model. + +| Flag | Description | +|------|-------------| +| `--system` | Show the system inference route instead of the user-facing route. When omitted, both routes are displayed. | + +--- + +## Additional Commands + + +### `openshell logs [NAME]` + +View sandbox logs. + +| Flag | Default | Description | +|------|---------|-------------| +| `[NAME]` | | Sandbox name (defaults to last-used sandbox). | +| `-n ` | `200` | Number of log lines to return. | +| `--tail` | | Stream live logs. | +| `--since ` | | Only show logs from this duration ago (e.g. 5m, 1h, 30s). | +| `--source ` | `all` | Filter by log source: "gateway", "sandbox", or "all" (default). Can be specified multiple times: --source gateway --source sandbox. | +| `--level ` | | Minimum log level to display: error, warn, info (default), debug, trace. | + +### `openshell status` + +Show gateway status and information. + + +### `openshell term` + +Launch the `OpenShell` interactive TUI. + +| Flag | Default | Description | +|------|---------|-------------| +| `--theme ` | `auto` | Color theme for the TUI: auto, dark, or light. Env: `OPENSHELL_THEME`. | + +### `openshell completions ` + +Generate shell completions. + +| Flag | Description | +|------|-------------| +| `` | Shell to generate completions for. | From 4bd0dec25dadb5420e6d89914a7ae727a7359958 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 12 Mar 2026 19:24:01 -0700 Subject: [PATCH 2/3] add mise to simplify more and update instructions --- .../skills/generate-cli-reference/SKILL.md | 39 +++++++++++-------- CONTRIBUTING.md | 34 +++++++++++----- tasks/ci.toml | 4 +- tasks/docs.toml | 36 +++++++++++++++++ 4 files changed, 86 insertions(+), 27 deletions(-) diff --git a/.agents/skills/generate-cli-reference/SKILL.md b/.agents/skills/generate-cli-reference/SKILL.md index 16d2fa8f..441eca5c 100644 --- a/.agents/skills/generate-cli-reference/SKILL.md +++ b/.agents/skills/generate-cli-reference/SKILL.md @@ -16,40 +16,39 @@ Run this after any change to CLI commands, flags, or doc comments in `crates/nav - Rust toolchain available (`cargo` on PATH, or use `mise exec --`) - The project compiles successfully -## Steps +## Quick Start -### 1. Build and run the generator +Regenerate and deploy in one command: ```bash -export PATH="$HOME/.cargo/bin:$PATH" -cargo run --bin openshell -- docs cli-reference > /tmp/cli-reference-generated.md +mise run docs:cli-reference ``` -Or with mise: +This builds the CLI binary, runs the hidden `docs cli-reference` subcommand, and copies the output to both target locations. + +### Check only (no changes) + +Verify the committed docs are up to date without modifying files: ```bash -mise exec -- cargo run --bin openshell -- docs cli-reference > /tmp/cli-reference-generated.md +mise run docs:cli-reference:check ``` -### 2. Deploy to documentation targets +This check also runs automatically as part of `mise run pre-commit`. -Copy the generated file to both locations: +### Manual steps (without mise) + +If you need to run the steps individually: ```bash +export PATH="$HOME/.cargo/bin:$PATH" +cargo run --bin openshell -- docs cli-reference > /tmp/cli-reference-generated.md cp /tmp/cli-reference-generated.md .agents/skills/openshell-cli/cli-reference-generated.md cp /tmp/cli-reference-generated.md docs/reference/cli-generated.md ``` Note: `.claude/skills/` is a symlink to `.agents/skills/`, so the first copy covers both. -### 3. Verify - -Spot-check that descriptions end with periods and the command tree looks correct: - -```bash -head -60 .agents/skills/openshell-cli/cli-reference-generated.md -``` - ## Target Files | File | Purpose | @@ -64,6 +63,14 @@ head -60 .agents/skills/openshell-cli/cli-reference-generated.md | `crates/navigator-cli/src/main.rs` | CLI definitions (commands, flags, doc comments) | | `crates/navigator-cli/src/cli_reference.rs` | Markdown generator logic | +## Automation + +| Trigger | What happens | +|---------|-------------| +| `mise run docs:cli-reference` | Regenerates and deploys to both targets. | +| `mise run docs:cli-reference:check` | Fails if committed docs are stale. | +| `mise run pre-commit` | Runs the staleness check alongside CI. | + ## Notes - The generator is a hidden subcommand (`openshell docs cli-reference`) that introspects clap's command tree at runtime. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8da8cd90..a918ea30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,15 +73,31 @@ k9s -n navigator These are the primary `mise` tasks for day-to-day development: -| Task | Purpose | -| ------------------ | ------------------------------------------------------- | -| `mise run cluster` | Bootstrap or incremental deploy | -| `mise run sandbox` | Create a sandbox on the running cluster | -| `mise run test` | Default test suite | -| `mise run e2e` | Default end-to-end test lane | -| `mise run ci` | Full local CI checks (lint, compile/type checks, tests) | -| `mise run docs` | Build and serve documentation locally | -| `mise run clean` | Clean build artifacts | +| Task | Purpose | +| --------------------------------- | ------------------------------------------------------- | +| `mise run cluster` | Bootstrap or incremental deploy | +| `mise run sandbox` | Create a sandbox on the running cluster | +| `mise run test` | Default test suite | +| `mise run e2e` | Default end-to-end test lane | +| `mise run ci` | Full local CI checks (lint, compile/type checks, tests) | +| `mise run docs` | Build and serve documentation locally | +| `mise run docs:cli-reference` | Regenerate CLI reference markdown from source | +| `mise run clean` | Clean build artifacts | + +## Generated Documentation + +The CLI reference markdown is auto-generated from the `///` doc comments on commands and flags in `crates/navigator-cli/src/main.rs`. After changing any CLI command, flag, or description, regenerate it: + +```bash +mise run docs:cli-reference +``` + +This writes to two locations: + +- `.agents/skills/openshell-cli/cli-reference-generated.md` (agent skills) +- `docs/reference/cli-generated.md` (user-facing docs) + +The pre-commit hook (`mise run pre-commit`) automatically checks for staleness and fails if the generated files are out of date. ## Project Structure diff --git a/tasks/ci.toml b/tasks/ci.toml index 515b5abd..2d1c5a54 100644 --- a/tasks/ci.toml +++ b/tasks/ci.toml @@ -42,6 +42,6 @@ depends = ["ci"] hide = true ["pre-commit"] -description = "Alias for ci" -depends = ["ci"] +description = "Run CI checks and verify generated docs" +depends = ["ci", "docs:cli-reference:check"] hide = true diff --git a/tasks/docs.toml b/tasks/docs.toml index 5dd1897d..7e59cb53 100644 --- a/tasks/docs.toml +++ b/tasks/docs.toml @@ -33,3 +33,39 @@ run = "uv run sphinx-autobuild docs _build/docs --port 8000 --open-browser" ["docs:clean"] description = "Remove built documentation" run = "rm -rf _build/docs" + +["docs:cli-reference"] +description = "Regenerate CLI reference markdown from source definitions" +run = """ +set -e +tmp=$(mktemp) +cargo run --bin openshell -- docs cli-reference > "$tmp" +cp "$tmp" .agents/skills/openshell-cli/cli-reference-generated.md +cp "$tmp" docs/reference/cli-generated.md +rm "$tmp" +echo "CLI reference regenerated:" +echo " .agents/skills/openshell-cli/cli-reference-generated.md" +echo " docs/reference/cli-generated.md" +""" + +["docs:cli-reference:check"] +description = "Check that CLI reference docs are up to date" +run = """ +set -e +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT +cargo run --bin openshell -- docs cli-reference > "$tmp" +stale="" +if ! diff -q "$tmp" .agents/skills/openshell-cli/cli-reference-generated.md >/dev/null 2>&1; then + stale="$stale .agents/skills/openshell-cli/cli-reference-generated.md" +fi +if ! diff -q "$tmp" docs/reference/cli-generated.md >/dev/null 2>&1; then + stale="$stale docs/reference/cli-generated.md" +fi +if [ -n "$stale" ]; then + echo "ERROR: CLI reference docs are stale:$stale" + echo "Run 'mise run docs:cli-reference' to regenerate." + exit 1 +fi +echo "CLI reference docs are up to date." +""" From f1600e4ae0990f8ee1066a59a348e72b0a383fa1 Mon Sep 17 00:00:00 2001 From: Miyoung Choi Date: Thu, 12 Mar 2026 19:29:20 -0700 Subject: [PATCH 3/3] fix(cli): apply rustfmt to cli_reference.rs Made-with: Cursor --- crates/navigator-cli/src/cli_reference.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/navigator-cli/src/cli_reference.rs b/crates/navigator-cli/src/cli_reference.rs index 06e81025..09e50a3b 100644 --- a/crates/navigator-cli/src/cli_reference.rs +++ b/crates/navigator-cli/src/cli_reference.rs @@ -88,10 +88,7 @@ fn write_tree_node(cmd: &Command, out: &mut String, prefix: &str, is_last: bool) writeln!(out, "{prefix}{connector}{name}{positionals}").unwrap(); - let children: Vec<_> = cmd - .get_subcommands() - .filter(|c| !c.is_hide_set()) - .collect(); + let children: Vec<_> = cmd.get_subcommands().filter(|c| !c.is_hide_set()).collect(); for (i, child) in children.iter().enumerate() { write_tree_node(child, out, &child_prefix, i == children.len() - 1); @@ -167,11 +164,7 @@ fn write_all_commands(root: &Command, out: &mut String, root_name: &str) { .collect(); for child in &children { - write_leaf_command( - child, - out, - &format!("{root_name} {}", group.get_name()), - ); + write_leaf_command(child, out, &format!("{root_name} {}", group.get_name())); } }