fabro/lib/crates/fabro-cli/src/args.rs
Bryan Helmkamp be084c1944
refactor(dev): decouple CLI reference generation
Expose the CLI reference renderer through a hidden fabro subcommand so fabro-dev can refresh docs without linking fabro-cli. Gate the fabro-dev binary behind the dev feature and update the cargo dev alias to opt into it explicitly.
2026-05-06 12:31:02 -04:00

1596 lines
42 KiB
Rust

use std::fmt;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use clap::{Args, Parser, Subcommand, ValueEnum};
use fabro_agent::cli::AgentArgs;
use fabro_config::{CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer};
use fabro_server::serve::DEFAULT_TCP_PORT;
use fabro_static::EnvVars;
use fabro_types::settings::cli::{OutputFormat, OutputVerbosity};
use fabro_types::settings::run::MergeStrategy;
use fabro_util::printer::Printer;
pub(crate) const LONG_VERSION: &str = concat!(
env!("CARGO_PKG_VERSION"),
" (",
env!("FABRO_GIT_SHA"),
" ",
env!("FABRO_BUILD_DATE"),
env!("FABRO_BUILD_PROFILE_SUFFIX"),
")"
);
#[derive(Parser)]
#[command(name = "fabro", version, long_version = LONG_VERSION)]
pub(crate) struct Cli {
#[command(flatten)]
pub(crate) globals: GlobalArgs,
#[command(subcommand)]
pub(crate) command: Option<Box<Commands>>,
}
impl Cli {
pub(crate) fn parse() -> Self {
<Self as Parser>::parse()
}
#[cfg(test)]
pub(crate) fn try_parse_from<I, T>(args: I) -> Result<Self, clap::Error>
where
I: IntoIterator<Item = T>,
T: Into<std::ffi::OsString> + Clone,
{
<Self as Parser>::try_parse_from(args)
}
}
#[derive(Args)]
pub(crate) struct GlobalArgs {
/// Output as JSON
#[arg(long, global = true, env = EnvVars::FABRO_JSON, value_parser = clap::builder::BoolishValueParser::new())]
pub json: bool,
/// Enable DEBUG-level logging (default is INFO)
#[arg(long, global = true, env = EnvVars::FABRO_DEBUG, value_parser = clap::builder::BoolishValueParser::new())]
pub debug: bool,
/// Disable automatic upgrade check
#[arg(long, global = true, env = EnvVars::FABRO_NO_UPGRADE_CHECK, value_parser = clap::builder::BoolishValueParser::new())]
pub no_upgrade_check: bool,
/// Suppress non-essential output
#[arg(long, global = true, env = EnvVars::FABRO_QUIET, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "verbose")]
pub quiet: bool,
/// Enable verbose output
#[arg(long, global = true, env = EnvVars::FABRO_VERBOSE, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "quiet")]
pub verbose: bool,
}
pub(crate) fn global_args_cli_layer(globals: &GlobalArgs) -> CliLayer {
let format = globals.json.then_some(OutputFormat::Json);
let verbosity = match (globals.quiet, globals.verbose) {
(true, _) => Some(OutputVerbosity::Quiet),
(_, true) => Some(OutputVerbosity::Verbose),
_ => None,
};
let output =
(format.is_some() || verbosity.is_some()).then_some(CliOutputLayer { format, verbosity });
let updates = globals
.no_upgrade_check
.then_some(CliUpdatesLayer { check: Some(false) });
let logging = globals.debug.then_some(CliLoggingLayer {
level: Some("debug".to_string()),
});
CliLayer {
output,
updates,
logging,
..CliLayer::default()
}
}
pub(crate) fn printer_from_verbosity(verbosity: OutputVerbosity) -> Printer {
match verbosity {
OutputVerbosity::Quiet => Printer::Quiet,
OutputVerbosity::Normal => Printer::Default,
OutputVerbosity::Verbose => Printer::Verbose,
}
}
pub(crate) fn require_no_json_override(process_local_json: bool) -> anyhow::Result<()> {
anyhow::ensure!(
!process_local_json,
"--json is not supported for this command"
);
Ok(())
}
pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration> {
let s = s.trim();
if s.is_empty() {
bail!("empty duration string");
}
let (num_str, unit) = s.split_at(s.len() - 1);
let num: i64 = num_str
.parse()
.with_context(|| format!("invalid duration: {s}"))?;
match unit {
"h" => Ok(chrono::Duration::hours(num)),
"d" => Ok(chrono::Duration::days(num)),
_ => bail!("invalid duration unit '{unit}' in '{s}' (expected 'h' or 'd')"),
}
}
pub(crate) fn default_web_url() -> String {
format!("http://127.0.0.1:{DEFAULT_TCP_PORT}")
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct StorageDirArgs {
/// Local storage directory (default: ~/.fabro/storage)
#[arg(long, env = EnvVars::FABRO_STORAGE_DIR)]
pub(crate) storage_dir: Option<PathBuf>,
}
impl StorageDirArgs {
pub(crate) fn as_deref(&self) -> Option<&Path> {
self.storage_dir.as_deref()
}
pub(crate) fn clone_path(&self) -> Option<PathBuf> {
self.storage_dir.clone()
}
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct ServerTargetArgs {
/// Fabro server target: http(s) URL or absolute Unix socket path
#[arg(long = "server", env = EnvVars::FABRO_SERVER)]
pub(crate) server: Option<String>,
}
impl ServerTargetArgs {
pub(crate) fn as_deref(&self) -> Option<&str> {
self.server.as_deref()
}
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct ServerConnectionArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum CliSandboxProvider {
Local,
Docker,
Daytona,
}
impl From<CliSandboxProvider> for fabro_sandbox::SandboxProvider {
fn from(value: CliSandboxProvider) -> Self {
match value {
CliSandboxProvider::Local => Self::Local,
CliSandboxProvider::Docker => Self::Docker,
CliSandboxProvider::Daytona => Self::Daytona,
}
}
}
impl From<fabro_sandbox::SandboxProvider> for CliSandboxProvider {
fn from(value: fabro_sandbox::SandboxProvider) -> Self {
match value {
fabro_sandbox::SandboxProvider::Local => Self::Local,
fabro_sandbox::SandboxProvider::Docker => Self::Docker,
fabro_sandbox::SandboxProvider::Daytona => Self::Daytona,
}
}
}
#[derive(Args)]
pub(crate) struct RunArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Path to a .fabro workflow file or .toml task config
#[arg(required = true)]
pub(crate) workflow: Option<PathBuf>,
/// Execute with simulated LLM backend
#[arg(long)]
pub(crate) dry_run: bool,
/// Auto-approve all human gates
#[arg(long)]
pub(crate) auto_approve: bool,
/// Override the workflow goal (available as {{ goal }} in prompts)
#[arg(long)]
pub(crate) goal: Option<String>,
/// Read the workflow goal from a file
#[arg(long, conflicts_with = "goal")]
pub(crate) goal_file: Option<PathBuf>,
/// Override default LLM model
#[arg(long)]
pub(crate) model: Option<String>,
/// Override default LLM provider
#[arg(long)]
pub(crate) provider: Option<String>,
/// Enable verbose output
#[arg(short, long)]
pub(crate) verbose: bool,
/// Sandbox for agent tools
#[arg(long, value_enum)]
pub(crate) sandbox: Option<CliSandboxProvider>,
/// Run directly in the source checkout without git checkpoints
#[arg(long, conflicts_with = "sandbox")]
pub(crate) in_place: bool,
/// Attach a label to this run (repeatable, format: KEY=VALUE)
#[arg(long = "label", value_name = "KEY=VALUE")]
pub(crate) label: Vec<String>,
/// Skip retro generation after the run
#[arg(long)]
pub(crate) no_retro: bool,
/// Keep the sandbox alive after the run finishes (for debugging)
#[arg(long)]
pub(crate) preserve_sandbox: bool,
/// Run the workflow in the background and print the run ID
#[arg(short = 'd', long)]
pub(crate) detach: bool,
/// Pre-generated run ID (used internally by --detach)
#[arg(long, hide = true)]
pub(crate) run_id: Option<String>,
}
#[derive(Args)]
pub(crate) struct PreflightArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Path to a .fabro workflow file or .toml task config
pub(crate) workflow: PathBuf,
/// Override the workflow goal (available as {{ goal }} in prompts)
#[arg(long)]
pub(crate) goal: Option<String>,
/// Read the workflow goal from a file
#[arg(long, conflicts_with = "goal")]
pub(crate) goal_file: Option<PathBuf>,
/// Override default LLM model
#[arg(long)]
pub(crate) model: Option<String>,
/// Override default LLM provider
#[arg(long)]
pub(crate) provider: Option<String>,
/// Enable verbose output
#[arg(short, long)]
pub(crate) verbose: bool,
/// Sandbox for agent tools
#[arg(long, value_enum)]
pub(crate) sandbox: Option<CliSandboxProvider>,
}
#[derive(Args)]
pub(crate) struct RunFilterArgs {
/// Only include runs started before this date (YYYY-MM-DD prefix match)
#[arg(long)]
pub(crate) before: Option<String>,
/// Filter by workflow name (substring match)
#[arg(long)]
pub(crate) workflow: Option<String>,
/// Filter by label (KEY=VALUE, repeatable, AND semantics)
#[arg(long = "label", value_name = "KEY=VALUE")]
pub(crate) label: Vec<String>,
/// Include orphan directories (no matching durable run)
#[arg(long)]
pub(crate) orphans: bool,
}
#[derive(Args)]
pub(crate) struct RunsListArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
#[command(flatten)]
pub(crate) filter: RunFilterArgs,
/// Show all runs, not just running (like docker ps -a)
#[arg(short = 'a', long)]
pub(crate) all: bool,
/// Only display run IDs
#[arg(short = 'q', long)]
pub(crate) quiet: bool,
}
#[derive(Args)]
pub(crate) struct RunsRemoveArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run IDs or workflow names to remove
#[arg(required = true)]
pub(crate) runs: Vec<String>,
/// Force removal of active runs
#[arg(short, long)]
pub(crate) force: bool,
}
#[derive(Args)]
pub(crate) struct RunsArchiveArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run IDs or workflow names to archive
#[arg(required = true)]
pub(crate) runs: Vec<String>,
}
#[derive(Args)]
pub(crate) struct RunsUnarchiveArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run IDs or workflow names to unarchive
#[arg(required = true)]
pub(crate) runs: Vec<String>,
}
#[derive(Args)]
pub(crate) struct LogsArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name (most recent run)
pub(crate) run: String,
/// Follow log output
#[arg(short, long)]
pub(crate) follow: bool,
/// Logs since timestamp or relative (e.g. "42m", "2h",
/// "2026-01-02T13:00:00Z")
#[arg(long)]
pub(crate) since: Option<String>,
/// Lines from end (default: all)
#[arg(short = 'n', long)]
pub(crate) tail: Option<usize>,
/// Formatted colored output with rendered assistant text
#[arg(short = 'p', long)]
pub(crate) pretty: bool,
}
#[derive(Args)]
pub(crate) struct ValidateArgs {
/// Path to the .fabro workflow file
pub(crate) workflow: PathBuf,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum GraphDirection {
/// Left to right
Lr,
/// Top to bottom
Tb,
}
impl fmt::Display for GraphDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lr => write!(f, "LR"),
Self::Tb => write!(f, "TB"),
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum GraphOutputFormat {
Svg,
}
impl fmt::Display for GraphOutputFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Svg => write!(f, "svg"),
}
}
}
#[derive(Args)]
pub(crate) struct GraphArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Path to the .fabro workflow file, .toml task config, or project workflow
/// name
pub(crate) workflow: PathBuf,
/// Output format
#[arg(long, value_enum, default_value_t = GraphOutputFormat::Svg)]
pub(crate) format: GraphOutputFormat,
/// Output file path (defaults to stdout)
#[arg(short, long)]
pub(crate) output: Option<PathBuf>,
/// Graph layout direction (overrides the DOT file's rankdir)
#[arg(short = 'd', long)]
pub(crate) direction: Option<GraphDirection>,
}
#[derive(Args)]
pub(crate) struct ParseArgs {
/// Path to the .fabro workflow file
pub(crate) workflow: PathBuf,
}
#[derive(Args)]
pub(crate) struct ArtifactListArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID (or prefix)
pub(crate) run_id: String,
/// Filter to artifacts from a specific node
#[arg(long)]
pub(crate) node: Option<String>,
/// Filter to artifacts from a specific retry attempt
#[arg(long)]
pub(crate) retry: Option<u32>,
}
#[derive(Args)]
pub(crate) struct ArtifactCpArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Source: RUN_ID (all artifacts) or RUN_ID:path (specific artifact)
pub(crate) source: String,
/// Destination directory (defaults to current directory)
#[arg(default_value = ".")]
pub(crate) dest: PathBuf,
/// Filter to artifacts from a specific node
#[arg(long)]
pub(crate) node: Option<String>,
/// Filter to artifacts from a specific retry attempt
#[arg(long)]
pub(crate) retry: Option<u32>,
/// Preserve {node_slug}/retry_{N}/ directory structure
#[arg(long)]
pub(crate) tree: bool,
}
#[derive(Args)]
pub(crate) struct CpArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Source: `<run-id>:<path>` or local path
pub(crate) src: String,
/// Destination: `<run-id>:<path>` or local path
pub(crate) dst: String,
/// Recurse into directories
#[arg(short, long)]
pub(crate) recursive: bool,
}
#[derive(Args)]
pub(crate) struct PreviewArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run: String,
/// Port number
pub(crate) port: u16,
/// Generate a signed URL (embeds auth token, no headers needed)
#[arg(long)]
pub(crate) signed: bool,
/// Signed URL expiry in seconds (default 3600, requires --signed)
#[arg(long, default_value = "3600", requires = "signed")]
pub(crate) ttl: i32,
/// Open URL in browser (implies --signed)
#[arg(long)]
pub(crate) open: bool,
}
#[derive(Args)]
pub(crate) struct SshArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run: String,
/// SSH access expiry in minutes (default 60)
#[arg(long, default_value = "60")]
pub(crate) ttl: f64,
/// Print the SSH command instead of connecting
#[arg(long)]
pub(crate) print: bool,
}
#[derive(Args)]
pub(crate) struct DiffArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run: String,
/// Show diff for a specific node
#[arg(long)]
pub(crate) node: Option<String>,
}
#[derive(Args)]
pub(crate) struct InspectArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name (most recent run)
pub(crate) run: String,
}
#[derive(Args)]
pub(crate) struct DumpArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name
pub(crate) run: String,
/// Output directory (must not exist or be empty)
#[arg(long, short)]
pub(crate) output: PathBuf,
}
#[derive(Args)]
pub(crate) struct SecretListArgs;
#[derive(Args)]
pub(crate) struct SecretRmArgs {
/// Name of the secret to remove
pub(crate) key: String,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub(crate) enum SecretTypeArg {
Environment,
File,
}
#[derive(Args)]
pub(crate) struct SecretSetArgs {
/// Name of the secret
pub(crate) key: String,
/// Value to store (omit to enter interactively)
pub(crate) value: Option<String>,
/// Read the secret value from stdin
#[arg(long, conflicts_with = "value")]
pub(crate) value_stdin: bool,
/// Secret storage type
#[arg(long, value_enum, default_value = "environment")]
pub(crate) r#type: SecretTypeArg,
/// Optional human-readable description
#[arg(long)]
pub(crate) description: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct ResumeArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or unambiguous prefix
pub(crate) run: String,
/// Run in the background and print the run ID
#[arg(short = 'd', long)]
pub(crate) detach: bool,
}
#[derive(Debug, Args)]
pub(crate) struct RewindArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID (or unambiguous prefix)
pub(crate) run_id: String,
/// Target checkpoint: node name, node@visit, or @ordinal (omit with --list)
pub(crate) target: Option<String>,
/// Show the checkpoint timeline instead of rewinding
#[arg(long)]
pub(crate) list: bool,
}
#[derive(Debug, Args)]
pub(crate) struct ForkArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID (or unambiguous prefix)
pub(crate) run_id: String,
/// Target checkpoint: node name, node@visit, or @ordinal (omit to fork from
/// latest)
pub(crate) target: Option<String>,
/// Show the checkpoint timeline instead of forking
#[arg(long)]
pub(crate) list: bool,
}
#[derive(Args)]
pub(crate) struct WaitArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name (most recent run)
pub(crate) run: String,
/// Maximum time to wait in seconds
#[arg(long, value_name = "SECONDS")]
pub(crate) timeout: Option<u64>,
/// Poll interval in milliseconds
#[arg(long, value_name = "MS", default_value = "1000")]
pub(crate) interval: u64,
}
#[derive(Args)]
pub(crate) struct SteerArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix to steer
pub(crate) run: String,
/// Steer message text (omit when --text-stdin is used)
pub(crate) text: Option<String>,
/// Read steer text from stdin instead of a positional arg
#[arg(long, conflicts_with = "text")]
pub(crate) text_stdin: bool,
/// Cancel the in-flight LLM stream / tool calls and deliver the message
/// as the next user turn (default: append to the steering queue).
#[arg(long)]
pub(crate) interrupt: bool,
}
#[derive(Args)]
pub(crate) struct WorkflowListArgs;
#[derive(Args)]
pub(crate) struct WorkflowCreateArgs {
/// Name of the workflow
pub(crate) name: String,
/// Goal description for the workflow
#[arg(short, long)]
pub(crate) goal: Option<String>,
}
#[derive(Args)]
pub(crate) struct ProviderLoginArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// LLM provider to authenticate with
#[arg(long)]
pub(crate) provider: fabro_model::Provider,
/// Read an API key from stdin instead of prompting
#[arg(long)]
pub(crate) api_key_stdin: bool,
}
#[derive(Args)]
pub(crate) struct SystemInfoArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct VersionArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
}
#[derive(Args)]
pub(crate) struct RunsPruneArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
#[command(flatten)]
pub(crate) filter: RunFilterArgs,
/// Only prune runs older than this duration (e.g. 24h, 7d). Default: 24h
/// when no explicit filters are set.
#[arg(
long,
value_name = "DURATION",
value_parser = parse_duration
)]
pub(crate) older_than: Option<chrono::Duration>,
/// Actually delete (default is dry-run)
#[arg(long)]
pub(crate) yes: bool,
}
#[derive(Args)]
pub(crate) struct DfArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
/// Show per-run breakdown
#[arg(short, long)]
pub(crate) verbose: bool,
}
#[derive(Args)]
pub(crate) struct SystemEventsArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
/// Filter by run ID (repeatable)
#[arg(long = "run-id")]
pub(crate) run_ids: Vec<String>,
}
#[derive(Args)]
pub(crate) struct SystemRepairArgs {
#[command(subcommand)]
pub(crate) command: SystemRepairCommand,
}
#[derive(Subcommand)]
pub(crate) enum SystemRepairCommand {
/// List runs that cannot be loaded from durable storage
Runs(SystemRepairRunsArgs),
}
#[derive(Args)]
pub(crate) struct SystemRepairRunsArgs {
#[command(flatten)]
pub(crate) connection: ServerConnectionArgs,
}
#[derive(Args)]
pub(crate) struct SettingsArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
}
#[derive(Args)]
pub(crate) struct PrCreateArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run_id: String,
/// LLM model for generating PR description
#[arg(long)]
pub(crate) model: Option<String>,
/// Create PR even if the run status is not succeeded/partially_succeeded
#[arg(short, long)]
pub(crate) force: bool,
}
#[derive(Args)]
pub(crate) struct PrViewArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run_id: String,
}
#[derive(Args)]
pub(crate) struct PrMergeArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run_id: String,
/// Merge method: merge, squash, or rebase
#[arg(long, value_enum, default_value_t = MergeStrategy::Squash)]
pub(crate) method: MergeStrategy,
}
#[derive(Args)]
pub(crate) struct PrCloseArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID or prefix
pub(crate) run_id: String,
}
#[derive(Args)]
pub(crate) struct StartArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name
pub(crate) run: String,
}
#[derive(Args)]
pub(crate) struct AttachArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Run ID prefix or workflow name
pub(crate) run: String,
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum RunWorkerMode {
Start,
Resume,
}
#[derive(Args)]
pub(crate) struct RunWorkerArgs {
/// Fabro server target: http(s) URL or absolute Unix socket path
#[arg(long)]
pub(crate) server: String,
/// Fabro storage directory for loading worker-visible secrets
#[arg(long, hide = true)]
pub(crate) storage_dir: Option<PathBuf>,
/// Run scratch directory
#[arg(long)]
pub(crate) run_dir: PathBuf,
/// Run ID
#[arg(long)]
pub(crate) run_id: fabro_types::RunId,
/// Worker mode
#[arg(long, value_enum)]
pub(crate) mode: RunWorkerMode,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct ModelListArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Filter by provider
#[arg(short, long)]
pub(crate) provider: Option<String>,
/// Search for models matching this string
#[arg(short, long)]
pub(crate) query: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct ModelTestArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Filter by provider
#[arg(short, long)]
pub(crate) provider: Option<String>,
/// Test a specific model
#[arg(short, long)]
pub(crate) model: Option<String>,
/// Number of model tests to run concurrently in bulk mode
#[arg(
short = 'j',
long,
default_value_t = 4,
value_parser = clap::builder::RangedU64ValueParser::<usize>::new().range(1..)
)]
pub(crate) jobs: usize,
/// Run a multi-turn tool-use test (catches reasoning round-trip bugs)
#[arg(long)]
pub(crate) deep: bool,
}
#[derive(Args)]
pub(crate) struct ExecArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
#[command(flatten)]
pub(crate) agent: AgentArgs,
}
#[derive(Args)]
pub(crate) struct UpgradeArgs {
/// Target version (e.g. "0.5.0", "v0.5.0", or "v0.177.0-alpha.1")
#[arg(long)]
pub(crate) version: Option<String>,
/// Include prereleases (alpha, beta, rc) when selecting the latest version
#[arg(long, conflicts_with = "version")]
pub(crate) prerelease: bool,
/// Upgrade even if already on the target version
#[arg(long)]
pub(crate) force: bool,
/// Preview what would happen without making changes
#[arg(long)]
pub(crate) dry_run: bool,
}
#[derive(Subcommand)]
pub(crate) enum RunCommands {
/// Launch a workflow run
Run(RunArgs),
/// Create a workflow run (allocate run dir, persist spec)
Create(RunArgs),
/// Start a created workflow run on the server
Start(StartArgs),
/// Attach to a running or finished workflow run
Attach(AttachArgs),
/// Internal: execute a single workflow run locally
#[command(name = "__run-worker", hide = true)]
RunWorker(RunWorkerArgs),
/// Show the diff of changes from a workflow run
#[command(hide = true)]
Diff(DiffArgs),
/// View the event log of a workflow run
Logs(LogsArgs),
/// Resume an interrupted workflow run
Resume(ResumeArgs),
/// Rewind a workflow run to an earlier checkpoint
Rewind(RewindArgs),
/// Fork a workflow run from an earlier checkpoint into a new run
Fork(ForkArgs),
/// Block until a workflow run completes
Wait(WaitArgs),
/// Steer a running agent mid-execution
Steer(SteerArgs),
}
impl RunCommands {
pub(crate) fn name(&self) -> &'static str {
match self {
Self::Run(_) => "run",
Self::Create(_) => "create",
Self::Start(_) => "start",
Self::Attach(_) => "attach",
Self::RunWorker(_) => "__run-worker",
Self::Diff(_) => "diff",
Self::Logs(_) => "logs",
Self::Resume(_) => "resume",
Self::Rewind(_) => "rewind",
Self::Steer(_) => "steer",
Self::Fork(_) => "fork",
Self::Wait(_) => "wait",
}
}
}
#[derive(Subcommand)]
pub(crate) enum SandboxCommand {
/// Copy files to/from a run's sandbox
Cp(CpArgs),
/// Get a preview URL for a port on a run's sandbox
Preview(PreviewArgs),
/// SSH into a run's sandbox
Ssh(SshArgs),
}
impl SandboxCommand {
pub(crate) fn name(&self) -> &'static str {
match self {
Self::Cp(_) => "sandbox cp",
Self::Preview(_) => "sandbox preview",
Self::Ssh(_) => "sandbox ssh",
}
}
}
#[derive(Subcommand)]
pub(crate) enum RunsCommands {
/// List workflow runs
#[command(hide = true)]
Ps(RunsListArgs),
/// Remove one or more workflow runs
Rm(RunsRemoveArgs),
/// Show detailed information about a workflow run
Inspect(InspectArgs),
/// Mark terminal runs as archived (reviewed, no further action needed).
/// Archived runs are hidden from default listings.
Archive(RunsArchiveArgs),
/// Restore archived runs to their prior terminal status.
Unarchive(RunsUnarchiveArgs),
}
impl RunsCommands {
pub(crate) fn name(&self) -> &'static str {
match self {
Self::Ps(_) => "ps",
Self::Rm(_) => "rm",
Self::Inspect(_) => "inspect",
Self::Archive(_) => "archive",
Self::Unarchive(_) => "unarchive",
}
}
}
#[derive(Subcommand)]
pub(crate) enum ModelsCommand {
/// List available models
List(ModelListArgs),
/// Test model availability by sending a simple prompt
Test(ModelTestArgs),
}
#[derive(Subcommand)]
pub(crate) enum Commands {
/// Run an agentic coding session
#[command(hide = true)]
Exec(ExecArgs),
#[command(flatten)]
RunCmd(RunCommands),
/// Validate run configuration without executing
Preflight(PreflightArgs),
/// Validate a workflow
Validate(ValidateArgs),
/// Render a workflow graph as SVG
Graph(GraphArgs),
/// Parse a DOT file and print its AST
#[command(hide = true)]
Parse(ParseArgs),
/// Inspect and copy run artifacts (screenshots, reports, traces)
Artifact(ArtifactNamespace),
/// Export a run's durable state to a directory
Dump(DumpArgs),
#[command(flatten)]
RunsCmd(RunsCommands),
/// List and test LLM models
Model {
#[command(subcommand)]
command: Option<ModelsCommand>,
},
/// Server operations
Server(ServerNamespace),
/// Check environment and integration health
Doctor(DoctorArgs),
/// Show client and server version information
Version(VersionArgs),
/// Set up the Fabro environment (LLMs, certs, GitHub)
Install {
#[command(flatten)]
args: InstallArgs,
#[command(subcommand)]
command: Option<InstallCommand>,
},
/// Uninstall Fabro from this machine
Uninstall(UninstallArgs),
/// Manage CLI authentication state
Auth(AuthNamespace),
/// Pull request operations
Pr(PrNamespace),
/// Manage server-owned secrets
Secret(SecretNamespace),
/// Inspect effective settings
Settings(SettingsArgs),
/// Workflow operations
Workflow(WorkflowNamespace),
/// Open the Discord community in the browser
Discord,
/// Open the docs website in the browser
Docs,
/// Upgrade fabro to the latest version
Upgrade(UpgradeArgs),
/// Repository commands
Repo(RepoNamespace),
/// Provider operations
Provider(ProviderNamespace),
/// Sandbox operations (cp, ssh, preview)
Sandbox {
#[command(subcommand)]
command: SandboxCommand,
},
/// Generate shell completions
Completion(CompletionArgs),
/// System maintenance commands
System(SystemNamespace),
/// Send a queued analytics event (internal)
#[command(name = "__send_analytics", hide = true)]
SendAnalytics {
/// Path to the JSON event file
path: PathBuf,
},
/// Send a queued panic event to Sentry (internal)
#[command(name = "__send_panic", hide = true)]
SendPanic {
/// Path to the JSON event file
path: PathBuf,
},
/// Print generated CLI reference Markdown (internal)
#[command(name = "__cli-reference", hide = true)]
CliReference,
/// Render a DOT graph to SVG (internal)
#[command(name = "__render-graph", hide = true)]
RenderGraph,
/// Build a panic event and write JSON to stdout (internal testing)
#[cfg(debug_assertions)]
#[command(name = "__test_panic", hide = true)]
TestPanic {
/// Panic message
message: String,
},
}
impl Commands {
pub(crate) fn name(&self) -> &'static str {
match self {
Self::Artifact(ns) => match &ns.command {
ArtifactCommand::List(_) => "artifact list",
ArtifactCommand::Cp(_) => "artifact cp",
},
Self::Dump(_) => "dump",
Self::Exec(_) => "exec",
Self::RunCmd(cmd) => cmd.name(),
Self::Preflight(_) => "preflight",
Self::Validate(_) => "validate",
Self::Graph(_) => "graph",
Self::Parse(_) => "parse",
Self::RunsCmd(cmd) => cmd.name(),
Self::Model { command } => match command {
Some(ModelsCommand::List(_)) => "model list",
Some(ModelsCommand::Test(_)) => "model test",
None => "model",
},
Self::Server(ns) => match &ns.command {
ServerCommand::Start(_) => "server start",
ServerCommand::Stop(_) => "server stop",
ServerCommand::Restart(_) => "server restart",
ServerCommand::Status(_) => "server status",
ServerCommand::Serve(_) => "server __serve",
},
Self::Doctor(_) => "doctor",
Self::Version(_) => "version",
Self::Repo(ns) => match &ns.command {
RepoCommand::Init(_) => "repo init",
RepoCommand::Deinit => "repo deinit",
},
Self::Install { command, .. } => match command {
None => "install",
Some(InstallCommand::Github(_)) => "install github",
},
Self::Uninstall(_) => "uninstall",
Self::Auth(ns) => match &ns.command {
AuthCommand::Login(_) => "auth login",
AuthCommand::Logout(_) => "auth logout",
AuthCommand::Status(_) => "auth status",
},
Self::Pr(ns) => match &ns.command {
PrCommand::Create(_) => "pr create",
PrCommand::View(_) => "pr view",
PrCommand::Merge(_) => "pr merge",
PrCommand::Close(_) => "pr close",
},
Self::Secret(ns) => match &ns.command {
SecretCommand::List(_) => "secret list",
SecretCommand::Rm(_) => "secret rm",
SecretCommand::Set(_) => "secret set",
},
Self::Settings(_) => "settings",
Self::Workflow(ns) => match &ns.command {
WorkflowCommand::List(_) => "workflow list",
WorkflowCommand::Create(_) => "workflow create",
},
Self::Discord => "discord",
Self::Docs => "docs",
Self::Upgrade(_) => "upgrade",
Self::Provider(ns) => match &ns.command {
ProviderCommand::Login(_) => "provider login",
},
Self::Sandbox { command } => command.name(),
Self::Completion(_) => "completion",
Self::System(ns) => match &ns.command {
SystemCommand::Info(_) => "system info",
SystemCommand::Prune(_) => "system prune",
SystemCommand::Df(_) => "system df",
SystemCommand::Events(_) => "system events",
SystemCommand::Repair(args) => match &args.command {
SystemRepairCommand::Runs(_) => "system repair runs",
},
},
Self::SendAnalytics { .. } => "__send_analytics",
Self::SendPanic { .. } => "__send_panic",
Self::CliReference => "__cli-reference",
Self::RenderGraph => "__render-graph",
#[cfg(debug_assertions)]
Self::TestPanic { .. } => "__test_panic",
}
}
}
#[derive(Args)]
pub(crate) struct PrNamespace {
#[command(subcommand)]
pub(crate) command: PrCommand,
}
#[derive(Subcommand)]
pub(crate) enum PrCommand {
/// Create a pull request from a completed run
Create(PrCreateArgs),
/// View pull request details
View(PrViewArgs),
/// Merge a pull request
Merge(PrMergeArgs),
/// Close a pull request
Close(PrCloseArgs),
}
#[derive(Args)]
pub(crate) struct ArtifactNamespace {
#[command(subcommand)]
pub(crate) command: ArtifactCommand,
}
#[derive(Subcommand)]
pub(crate) enum ArtifactCommand {
/// List artifacts for a workflow run
#[command(alias = "ls")]
List(ArtifactListArgs),
/// Copy artifacts from a workflow run
Cp(ArtifactCpArgs),
}
#[derive(Args)]
pub(crate) struct SecretNamespace {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
#[command(subcommand)]
pub(crate) command: SecretCommand,
}
#[derive(Subcommand)]
pub(crate) enum SecretCommand {
/// List secret names
#[command(alias = "ls")]
List(SecretListArgs),
/// Remove a secret
Rm(SecretRmArgs),
/// Set a secret value
Set(SecretSetArgs),
}
#[derive(Args)]
pub(crate) struct ServerNamespace {
#[command(subcommand)]
pub(crate) command: ServerCommand,
}
use fabro_server::serve::ServeArgs;
#[derive(Args)]
pub(crate) struct ServerStartArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
/// Run in the foreground instead of daemonizing
#[arg(long)]
pub(crate) foreground: bool,
#[command(flatten)]
pub(crate) serve_args: ServeArgs,
}
#[derive(Args)]
pub(crate) struct ServerStopArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
/// Seconds to wait for graceful shutdown before SIGKILL
#[arg(long, default_value = "10")]
pub(crate) timeout: u64,
}
#[derive(Args)]
pub(crate) struct ServerStatusArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
/// Output as JSON
#[arg(long)]
pub(crate) json: bool,
}
#[derive(Args)]
pub(crate) struct ServerRestartArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
/// Seconds to wait for graceful shutdown before SIGKILL
#[arg(long, default_value = "10")]
pub(crate) timeout: u64,
/// Run in the foreground instead of daemonizing
#[arg(long)]
pub(crate) foreground: bool,
#[command(flatten)]
pub(crate) serve_args: ServeArgs,
}
#[derive(Args)]
pub(crate) struct ServerServeArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
#[command(flatten)]
pub(crate) serve_args: ServeArgs,
}
#[derive(Subcommand)]
pub(crate) enum ServerCommand {
/// Start the HTTP API server
Start(ServerStartArgs),
/// Stop the HTTP API server
Stop(ServerStopArgs),
/// Stop and restart the HTTP API server
Restart(ServerRestartArgs),
/// Show server status
Status(ServerStatusArgs),
/// Internal: run the server process (spawned by `start`)
#[command(name = "__serve", hide = true)]
Serve(ServerServeArgs),
}
#[derive(Args)]
pub(crate) struct SystemNamespace {
#[command(subcommand)]
pub(crate) command: SystemCommand,
}
#[derive(Subcommand)]
pub(crate) enum SystemCommand {
/// Show server runtime information
Info(SystemInfoArgs),
/// Delete old workflow runs
Prune(RunsPruneArgs),
/// Show disk usage
Df(DfArgs),
/// Stream run events from the server
Events(SystemEventsArgs),
/// Inspect and repair durable server data
Repair(SystemRepairArgs),
}
#[derive(Args)]
pub(crate) struct WorkflowNamespace {
#[command(subcommand)]
pub(crate) command: WorkflowCommand,
}
#[derive(Subcommand)]
pub(crate) enum WorkflowCommand {
/// List available workflows
#[command(alias = "ls")]
List(WorkflowListArgs),
/// Create a new workflow
Create(WorkflowCreateArgs),
}
#[derive(Args)]
pub(crate) struct RepoNamespace {
#[command(subcommand)]
pub(crate) command: RepoCommand,
}
#[derive(Subcommand)]
pub(crate) enum RepoCommand {
/// Initialize a new project
Init(RepoInitArgs),
/// Remove .fabro/ project directory
Deinit,
}
#[derive(Args)]
pub(crate) struct RepoInitArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
}
#[derive(Args)]
pub(crate) struct DoctorArgs {
#[command(flatten)]
pub(crate) target: ServerTargetArgs,
/// Show detailed information for each check
#[arg(short, long)]
pub(crate) verbose: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum InstallGitHubStrategyArg {
#[value(name = "token")]
Token,
App,
}
#[derive(Subcommand, Debug, Clone)]
pub(crate) enum InstallCommand {
/// Configure GitHub integration (token or GitHub App)
Github(InstallGithubArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct InstallGithubArgs {
/// GitHub authentication strategy (requires --non-interactive)
#[arg(long)]
pub(crate) strategy: Option<InstallGitHubStrategyArg>,
/// GitHub App owner: `personal` or `org:<slug>` (app only, requires
/// --non-interactive)
#[arg(long)]
pub(crate) owner: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct InstallNonInteractiveArgs {
#[arg(long, hide = true)]
pub(crate) llm_provider: Option<fabro_model::Provider>,
#[arg(long, hide = true)]
pub(crate) llm_api_key_stdin: bool,
#[arg(long, hide = true)]
pub(crate) llm_api_key_env: Option<String>,
#[arg(long, hide = true)]
pub(crate) github_strategy: Option<InstallGitHubStrategyArg>,
#[arg(long, hide = true)]
pub(crate) github_owner: Option<String>,
#[arg(long, hide = true)]
pub(crate) github_username: Option<String>,
#[arg(long, hide = true)]
pub(crate) overwrite_settings: bool,
#[arg(long, hide = true)]
pub(crate) keep_existing_settings: bool,
#[arg(long, hide = true)]
pub(crate) run_doctor: bool,
}
#[derive(Args)]
pub(crate) struct InstallArgs {
#[command(flatten)]
pub(crate) storage_dir: StorageDirArgs,
/// Base URL for the web UI (used for OAuth callback URLs and generated
/// settings)
#[arg(long, default_value_t = default_web_url())]
pub(crate) web_url: String,
/// Run install without prompts; use hidden scripted flags for inputs
#[arg(long, global = true)]
pub(crate) non_interactive: bool,
#[command(flatten)]
pub(crate) scripted: InstallNonInteractiveArgs,
}
#[derive(Args)]
pub(crate) struct UninstallArgs {
/// Skip confirmation prompt
#[arg(long)]
pub(crate) yes: bool,
}
#[derive(Args)]
pub(crate) struct ProviderNamespace {
#[command(subcommand)]
pub(crate) command: ProviderCommand,
}
#[derive(Args)]
pub(crate) struct AuthNamespace {
#[command(subcommand)]
pub(crate) command: AuthCommand,
}
#[derive(Subcommand)]
pub(crate) enum AuthCommand {
/// Log in to a Fabro server
Login(AuthLoginArgs),
/// Log out from a Fabro server
Logout(AuthLogoutArgs),
/// Show offline CLI auth status
Status(AuthStatusArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct AuthLoginArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Log in with a dev-token instead of browser OAuth
#[arg(long, conflicts_with_all = ["no_browser", "timeout"])]
pub(crate) dev_token: Option<String>,
/// Print the browser URL instead of opening it automatically
#[arg(long)]
pub(crate) no_browser: bool,
/// Timeout in seconds waiting for the browser flow to complete
#[arg(long, default_value_t = 300)]
pub(crate) timeout: u64,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct AuthLogoutArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
/// Log out from every stored server
#[arg(long)]
pub(crate) all: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub(crate) struct AuthStatusArgs {
#[command(flatten)]
pub(crate) server: ServerTargetArgs,
}
#[derive(Subcommand)]
pub(crate) enum ProviderCommand {
/// Log in to an LLM provider
Login(ProviderLoginArgs),
}
#[derive(Args)]
pub(crate) struct CompletionArgs {
/// Shell to generate completions for
pub shell: clap_complete::Shell,
}