use clap::{Args, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "xbp",
version,
about = "Deploy, operate, and debug services with one CLI.",
long_about = "XBP is an operations-first CLI for deployments, diagnostics, service orchestration,\nnetwork controls, and runtime observability.",
disable_help_subcommand = false,
next_line_help = true,
help_template = "{before-help}{name} {version}\n{about-with-newline}\
{usage-heading} {usage}\n\n\
{all-args}\
{after-help}",
after_help = "Quick start:\n xbp diag\n xbp services\n xbp service start <name>\n xbp api install --port 8080\n\nUse `xbp <command> -h` for command-specific examples."
)]
pub struct Cli {
#[arg(long, global = true, help = "Enable verbose debugging output")]
pub debug: bool,
#[arg(short = 'l', help = "List pm2 processes")]
pub list: bool,
#[arg(short = 'p', long = "port", help = "Filter by port number")]
pub port: Option<u16>,
#[arg(long, help = "Open logs directory")]
pub logs: bool,
#[command(subcommand)]
pub command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(about = "Inspect or manage listening ports")]
Ports(PortsCmd),
#[command(
about = "Analyze the current git worktree and create a conventional commit",
visible_alias = "c"
)]
Commit(CommitCmd),
#[command(about = "Initialize an XBP project in the current directory")]
Init,
#[command(about = "Install common dependencies for host setup")]
Setup,
#[command(about = "Redeploy one service or the entire project")]
Redeploy {
#[arg(
help = "Service name to redeploy (optional, uses legacy redeploy.sh if not provided)"
)]
service_name: Option<String>,
},
#[command(about = "Run the legacy remote redeploy workflow over SSH")]
RedeployV2(RedeployV2Cmd),
#[command(about = "Inspect project/global config and manage provider keys")]
Config(ConfigCmd),
#[command(
about = "Install supported host packages or project tooling",
after_help = crate::commands::INSTALL_COMMAND_AFTER_HELP
)]
Install {
#[arg(short = 'l', long = "list", help = "List installable targets and exit")]
list: bool,
#[arg(help = "Install target (leave empty to show installable options)")]
package: Option<String>,
},
#[command(about = "Tail local or remote logs")]
Logs(LogsCmd),
#[command(
about = "Open an interactive remote shell over SSH",
visible_alias = "shell"
)]
Ssh(SshCmd),
#[command(about = "Open or manage cloudflared TCP forwarders")]
Cloudflared(CloudflaredCmd),
#[command(about = "List PM2 processes")]
List,
#[command(about = "Fetch an HTTP endpoint with sane defaults")]
Curl(CurlCmd),
#[command(about = "List configured services from project config")]
Services,
#[command(about = "Run service-level commands (build/install/start/dev)")]
Service {
#[arg(help = "Command to run: build, install, start, dev, or --help")]
command: Option<String>,
#[arg(help = "Service name")]
service_name: Option<String>,
},
#[command(about = "Manage NGINX site configs and upstream mappings")]
Nginx(NginxCmd),
#[command(about = "Manage host network configuration and floating IPs")]
Network(NetworkCmd),
#[command(about = "Run full system diagnostics and readiness checks")]
Diag(DiagCmd),
#[command(about = "Run health-check monitoring commands")]
Monitor(MonitorCmd),
#[command(about = "Capture a PM2 snapshot for later restore")]
Snapshot,
#[command(about = "Restore PM2 state from dump or latest snapshot")]
Resurrect,
#[command(about = "Stop a PM2 process by name or stop all")]
Stop {
#[arg(help = "PM2 process name or 'all' (default: all)")]
target: Option<String>,
},
#[command(about = "Flush PM2 logs globally or for a specific process")]
Flush {
#[arg(help = "Optional PM2 process name")]
target: Option<String>,
},
#[command(about = "Run login flow against configured XBP API")]
Login,
#[command(
about = "Inspect, reconcile, or bump project versions",
visible_alias = "v"
)]
Version(VersionCmd),
#[command(about = "Run configured npm/crates publish workflows for the current XBP project")]
Publish(PublishCmd),
#[command(about = "Show PM2 environment by name or numeric id")]
Env {
#[arg(help = "PM2 process name or id")]
target: String,
},
#[command(about = "Tail app logs or Kafka logs")]
Tail(TailCmd),
#[command(about = "Start a binary/process under PM2")]
Start {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
#[command(about = "Generate helper artifacts such as systemd units")]
Generate(GenerateCmd),
#[cfg(feature = "secrets")]
#[command(about = "Manage env vars and GitHub Actions environment variables (feature-gated)")]
Secrets(SecretsCmd),
#[command(about = "Manage DNS providers and records")]
Dns(DnsCmd),
#[command(about = "Discover and inspect registered domains")]
Domains(DomainsCmd),
#[command(
about = "Generate 'what did I get done' Markdown report from git commits across repos"
)]
Done(DoneCmd),
#[cfg(feature = "kubernetes")]
#[command(about = "Experimental Kubernetes cluster manager (feature-gated)")]
Kubernetes(KubernetesCmd),
#[cfg(feature = "nordvpn")]
#[command(about = "NordVPN meshnet setup and passthrough (feature-gated)")]
Nordvpn(NordvpnCmd),
#[cfg(feature = "monitoring")]
Monitoring(MonitoringCmd),
#[command(about = "Manage the XBP API server")]
Api(ApiCmd),
#[cfg(feature = "docker")]
#[command(about = "Pass-through wrapper around the Docker CLI")]
Docker(DockerCmd),
}
pub fn command_label(command: &Commands) -> &'static str {
match command {
Commands::Ports(_) => "ports",
Commands::Commit(_) => "commit",
Commands::Init => "init",
Commands::Setup => "setup",
Commands::Redeploy { .. } => "redeploy",
Commands::RedeployV2(_) => "redeploy-v2",
Commands::Config(_) => "config",
Commands::Install { .. } => "install",
Commands::Logs(_) => "logs",
Commands::Ssh(_) => "ssh",
Commands::Cloudflared(_) => "cloudflared",
Commands::List => "list",
Commands::Curl(_) => "curl",
Commands::Services => "services",
Commands::Service { .. } => "service",
Commands::Nginx(_) => "nginx",
Commands::Network(_) => "network",
Commands::Diag(_) => "diag",
Commands::Monitor(_) => "monitor",
Commands::Snapshot => "snapshot",
Commands::Resurrect => "resurrect",
Commands::Stop { .. } => "stop",
Commands::Flush { .. } => "flush",
Commands::Login => "login",
Commands::Version(_) => "version",
Commands::Publish(_) => "publish",
Commands::Env { .. } => "env",
Commands::Tail(_) => "tail",
Commands::Start { .. } => "start",
Commands::Generate(_) => "generate",
#[cfg(feature = "secrets")]
Commands::Secrets(_) => "secrets",
Commands::Dns(_) => "dns",
Commands::Domains(_) => "domains",
Commands::Done(_) => "done",
#[cfg(feature = "kubernetes")]
Commands::Kubernetes(_) => "kubernetes",
#[cfg(feature = "nordvpn")]
Commands::Nordvpn(_) => "nordvpn",
#[cfg(feature = "monitoring")]
Commands::Monitoring(_) => "monitoring",
Commands::Api(_) => "api",
#[cfg(feature = "docker")]
Commands::Docker(_) => "docker",
}
}
#[derive(Args, Debug)]
pub struct CommitCmd {
#[arg(
long,
help = "Generate and print the conventional commit message without creating a git commit"
)]
pub dry_run: bool,
#[arg(
short = 'p',
long,
help = "Push after committing, or push pending local commits when nothing new needs committing"
)]
pub push: bool,
#[arg(long, help = "Skip OpenRouter and use local heuristics only")]
pub no_ai: bool,
#[arg(
long,
default_value = "openai/gpt-4o-mini",
help = "OpenRouter model override used for commit generation"
)]
pub model: String,
#[arg(
long,
help = "Force the conventional commit scope (for example: cli, api, docs)"
)]
pub scope: Option<String>,
}
#[derive(Args, Debug)]
pub struct PortsCmd {
#[arg(short = 'p', long = "port")]
pub port: Option<u16>,
#[arg(long = "kill")]
pub kill: bool,
#[arg(short = 'n', long = "nginx")]
pub nginx: bool,
#[arg(
long = "full",
help = "Show one unified ports view (reconciled listeners + exposure + security flags)"
)]
pub full: bool,
#[arg(
long = "no-local",
help = "Exclude connections where LocalAddr equals RemoteAddr"
)]
pub no_local: bool,
#[arg(
long = "exposure",
help = "Diagnose external exposure per port (binding + firewall layer)"
)]
pub exposure: bool,
}
#[derive(Args, Debug)]
pub struct ConfigCmd {
#[arg(
long,
help = "Show the current project config instead of opening global XBP paths"
)]
pub project: bool,
#[arg(long, help = "Print global XBP paths without opening them")]
pub no_open: bool,
#[command(subcommand)]
pub provider: Option<ConfigProviderCmd>,
}
#[derive(Subcommand, Debug)]
pub enum ConfigProviderCmd {
#[command(about = "Manage the OpenRouter API key used by AI-enabled commands")]
Openrouter(ConfigSecretCmd),
#[command(about = "Manage the GitHub OAuth2 token used for release automation")]
Github(ConfigSecretCmd),
#[command(about = "Manage Cloudflare API credentials used by secrets, DNS, and domains")]
Cloudflare(CloudflareConfigCmd),
#[command(
about = "Manage the Linear API key used for release-note issue linking and initiative publishing"
)]
Linear(LinearConfigCmd),
#[command(about = "Manage npm registry auth and guided npm publish config")]
Npm(RegistryConfigCmd),
#[command(about = "Manage crates.io auth and guided crate publish config")]
Crates(RegistryConfigCmd),
}
#[derive(Args, Debug)]
pub struct ConfigSecretCmd {
#[command(subcommand)]
pub action: ConfigSecretAction,
}
#[derive(Subcommand, Debug)]
pub enum ConfigSecretAction {
#[command(about = "Set provider key (omit value to enter it securely)")]
SetKey {
#[arg(help = "Provider key/token value")]
key: Option<String>,
},
#[command(about = "Delete the stored provider key")]
DeleteKey,
#[command(about = "Show whether a key is configured (masked by default)")]
Show {
#[arg(long, help = "Print full key/token value (not masked)")]
raw: bool,
},
}
#[derive(Args, Debug)]
pub struct CloudflareConfigCmd {
#[command(subcommand)]
pub action: CloudflareConfigAction,
}
#[derive(Subcommand, Debug)]
pub enum CloudflareConfigAction {
#[command(about = "Set Cloudflare API token (omit value to enter it securely)")]
SetKey {
#[arg(help = "Cloudflare API token")]
key: Option<String>,
},
#[command(about = "Delete the stored Cloudflare API token")]
DeleteKey,
#[command(about = "Show whether a Cloudflare API token is configured")]
ShowKey {
#[arg(long, help = "Print full token value (not masked)")]
raw: bool,
},
#[command(about = "Set the default Cloudflare account ID")]
SetAccountId {
#[arg(help = "Cloudflare account ID")]
account_id: Option<String>,
},
#[command(about = "Delete the stored default Cloudflare account ID")]
DeleteAccountId,
#[command(about = "Show whether a Cloudflare account ID is configured")]
ShowAccountId {
#[arg(long, help = "Print full account ID value (not masked)")]
raw: bool,
},
}
#[derive(Args, Debug)]
pub struct LinearConfigCmd {
#[command(subcommand)]
pub action: LinearConfigAction,
}
#[derive(Subcommand, Debug)]
pub enum LinearConfigAction {
#[command(about = "Set Linear API key (omit value to enter it securely)")]
SetKey {
#[arg(help = "Linear API key/token value")]
key: Option<String>,
},
#[command(about = "Delete the stored Linear API key")]
DeleteKey,
#[command(about = "Show whether a Linear API key is configured (masked by default)")]
Show {
#[arg(long, help = "Print full key/token value (not masked)")]
raw: bool,
},
#[command(
name = "select-initiative",
about = "Pick a Linear initiative for the current repo and save it to .xbp/xbp.yaml"
)]
SelectInitiative,
}
#[derive(Args, Debug)]
pub struct RegistryConfigCmd {
#[command(subcommand)]
pub action: RegistryConfigAction,
}
#[derive(Subcommand, Debug)]
pub enum RegistryConfigAction {
#[command(about = "Set registry token/key (omit value to enter it securely)")]
SetKey {
#[arg(help = "Registry token value")]
key: Option<String>,
},
#[command(about = "Delete the stored registry token")]
DeleteKey,
#[command(about = "Show whether a registry token is configured (masked by default)")]
Show {
#[arg(long, help = "Print full token value (not masked)")]
raw: bool,
},
#[command(
name = "setup-release",
about = "Interactively configure project publish settings in .xbp/xbp.yaml"
)]
SetupRelease,
}
#[derive(Args, Debug)]
pub struct CurlCmd {
#[arg(help = "URL or domain to fetch, e.g. example.com or https://example.com/api")]
pub url: Option<String>,
#[arg(long, help = "Disable the default 15 second timeout")]
pub no_timeout: bool,
}
#[derive(Args, Debug)]
#[command(subcommand_precedence_over_arg = true)]
pub struct VersionCmd {
#[arg(
help = "Show versions, bump with major/minor/patch, or set an explicit version like 1.2.3"
)]
pub target: Option<String>,
#[arg(
short = 'v',
long = "version",
help = "Explicit version target; equivalent to the positional version value and overrides it when both are provided"
)]
pub explicit_version: Option<String>,
#[arg(long, help = "Show normalized git tags from `git tag --list`")]
pub git: bool,
#[command(subcommand)]
pub command: Option<VersionSubCommand>,
}
#[derive(Subcommand, Debug)]
pub enum VersionSubCommand {
#[command(
about = "Create and push a git tag for this version, then create a GitHub release",
visible_alias = "r"
)]
Release(VersionReleaseCmd),
#[command(
about = "Manage Rust workspace release/version drift, sync, validation, and publish flow",
arg_required_else_help = true
)]
Workspace(VersionWorkspaceCmd),
}
#[derive(Args, Debug)]
pub struct VersionReleaseCmd {
#[arg(
long,
help = "Release this version instead of auto-detecting from tracked files"
)]
pub version: Option<String>,
#[arg(
long,
help = "Allow releasing with uncommitted changes in the working tree"
)]
pub allow_dirty: bool,
#[arg(long, help = "Release title (defaults to <version> - <repo>)")]
pub title: Option<String>,
#[arg(long, help = "Release notes body (Markdown)")]
pub notes: Option<String>,
#[arg(long, help = "Read release notes body from a file")]
pub notes_file: Option<PathBuf>,
#[arg(long, help = "Create as draft release")]
pub draft: bool,
#[arg(long, help = "Mark release as pre-release")]
pub prerelease: bool,
#[arg(
long,
help = "Run configured npm/crates publish workflows before creating the GitHub release"
)]
pub publish: bool,
#[arg(
long,
value_enum,
default_value_t = VersionReleaseLatest::Legacy,
help = "Control GitHub latest flag: true, false, or legacy"
)]
pub make_latest: VersionReleaseLatest,
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum VersionReleaseLatest {
True,
False,
Legacy,
}
#[derive(Args, Debug)]
pub struct PublishCmd {
#[arg(
long,
help = "Validate and print what would publish without uploading packages"
)]
pub dry_run: bool,
#[arg(
long,
help = "Allow publish workflows to run with a dirty working tree"
)]
pub allow_dirty: bool,
#[arg(long, help = "Limit publishing to one target: npm or crates")]
pub target: Option<String>,
}
#[derive(Args, Debug)]
#[command(
after_help = "Examples:\n xbp version workspace check --repo C:/Users/floris/Documents/GitHub/athena\n xbp version workspace sync --version 3.16.5\n xbp version workspace sync --version 3.16.5 --write\n xbp version workspace validate --cargo-check --package-dry-run\n xbp version workspace publish plan\n xbp version workspace publish run --dry-run\n xbp version workspace publish run --from athena-s3"
)]
pub struct VersionWorkspaceCmd {
#[command(subcommand)]
pub command: VersionWorkspaceSubCommand,
}
#[derive(Args, Debug, Clone, Default)]
pub struct VersionWorkspaceTargetArgs {
#[arg(
long,
help = "Workspace repo root to inspect (defaults to current project root)"
)]
pub repo: Option<PathBuf>,
#[arg(long, help = "Emit machine-readable JSON output")]
pub json: bool,
}
#[derive(Subcommand, Debug)]
pub enum VersionWorkspaceSubCommand {
#[command(about = "Detect workspace release drift and exit non-zero when mismatches exist")]
Check(VersionWorkspaceCheckCmd),
#[command(about = "Preview or apply workspace-wide version alignment")]
Sync(VersionWorkspaceSyncCmd),
#[command(about = "Run structural and optional cargo validation for workspace publishability")]
Validate(VersionWorkspaceValidateCmd),
#[command(about = "Plan or execute crates.io publishing for workspace packages")]
Publish(VersionWorkspacePublishCmd),
}
#[derive(Args, Debug)]
pub struct VersionWorkspaceCheckCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
#[arg(
long,
help = "Expected release version (defaults to the root package version)"
)]
pub version: Option<String>,
}
#[derive(Args, Debug)]
pub struct VersionWorkspaceSyncCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
#[arg(
long,
help = "Target release version (defaults to the root package version)"
)]
pub version: Option<String>,
#[arg(
long,
help = "Write changes to disk instead of previewing the sync plan"
)]
pub write: bool,
}
#[derive(Args, Debug)]
pub struct VersionWorkspaceValidateCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
#[arg(long, help = "Limit cargo validation to a single package name")]
pub package: Option<String>,
#[arg(long, help = "Run `cargo check -q` as part of validation")]
pub cargo_check: bool,
#[arg(
long,
help = "Run `cargo publish --dry-run --locked` for publishable packages"
)]
pub package_dry_run: bool,
}
#[derive(Args, Debug)]
#[command(arg_required_else_help = true)]
pub struct VersionWorkspacePublishCmd {
#[command(subcommand)]
pub command: VersionWorkspacePublishSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum VersionWorkspacePublishSubCommand {
#[command(about = "Show publish order, crates.io visibility, and blockers without publishing")]
Plan(VersionWorkspacePublishPlanCmd),
#[command(about = "Publish workspace packages in dependency order")]
Run(VersionWorkspacePublishRunCmd),
}
#[derive(Args, Debug)]
pub struct VersionWorkspacePublishPlanCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
}
#[derive(Args, Debug)]
pub struct VersionWorkspacePublishRunCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
#[arg(long, help = "Preview publish actions without calling cargo publish")]
pub dry_run: bool,
#[arg(
long,
help = "Start publishing from this package in the computed order"
)]
pub from: Option<String>,
#[arg(long, help = "Publish only this package")]
pub only: Option<String>,
#[arg(long, help = "Continue publishing remaining packages after a failure")]
pub continue_on_error: bool,
#[arg(long, help = "Allow publishing from a dirty worktree")]
pub allow_dirty: bool,
#[arg(
long,
default_value_t = 180.0,
help = "How long to wait for each published version to become visible on crates.io"
)]
pub timeout_seconds: f64,
#[arg(
long,
default_value_t = 5.0,
help = "How often to poll crates.io for the just-published version"
)]
pub poll_interval_seconds: f64,
}
#[derive(Args, Debug)]
pub struct RedeployV2Cmd {
#[arg(short = 'p', long = "password")]
pub password: Option<String>,
#[arg(short = 'u', long = "username")]
pub username: Option<String>,
#[arg(short = 'h', long = "host")]
pub host: Option<String>,
#[arg(short = 'd', long = "project-dir")]
pub project_dir: Option<String>,
}
#[derive(Args, Debug)]
pub struct LogsCmd {
#[arg()]
pub project: Option<String>,
#[arg(long = "ssh-host", help = "SSH host to stream logs from")]
pub ssh_host: Option<String>,
#[arg(long = "ssh-username", help = "SSH username for remote host")]
pub ssh_username: Option<String>,
#[arg(long = "ssh-password", help = "SSH password for remote host")]
pub ssh_password: Option<String>,
}
#[derive(Args, Debug)]
pub struct SshCmd {
#[arg(long = "host", alias = "ssh-host", help = "SSH host or IP address")]
pub ssh_host: Option<String>,
#[arg(
long = "port",
default_value_t = 22,
help = "SSH port for direct connections"
)]
pub ssh_port: u16,
#[arg(
long = "username",
alias = "ssh-username",
help = "SSH username for the remote host"
)]
pub ssh_username: Option<String>,
#[arg(
long = "password",
alias = "ssh-password",
help = "SSH password (omit to use stored config or a secure prompt)"
)]
pub ssh_password: Option<String>,
#[arg(
long,
help = "Path to a private key file to use instead of password auth"
)]
pub private_key: Option<PathBuf>,
#[arg(long, help = "Passphrase for --private-key when required")]
pub private_key_passphrase: Option<String>,
#[arg(
long,
help = "Run this remote command in a PTY instead of opening the default login shell"
)]
pub command: Option<String>,
#[arg(
long,
help = "TERM value sent to the server (default: TERM env var or xterm-256color)"
)]
pub term: Option<String>,
#[arg(long, help = "Disable SSH host key verification")]
pub no_host_key_check: bool,
#[arg(
long,
help = "Pin the SSH host key as a base64 blob when using tunnels or first-connect flows"
)]
pub host_key: Option<String>,
#[arg(
long,
help = "Path to a known_hosts file used for SSH host verification"
)]
pub known_hosts_file: Option<PathBuf>,
#[arg(
long,
help = "Cloudflare Access hostname used to open a local cloudflared TCP forwarder"
)]
pub cloudflared_hostname: Option<String>,
#[arg(long, help = "Override the cloudflared binary path")]
pub cloudflared_binary: Option<PathBuf>,
#[arg(
long,
help = "Optional destination host:port passed to cloudflared access tcp"
)]
pub cloudflared_destination: Option<String>,
}
#[derive(Args, Debug)]
#[command(arg_required_else_help = true)]
pub struct CloudflaredCmd {
#[command(subcommand)]
pub command: CloudflaredSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CloudflaredSubCommand {
#[command(about = "Start a local cloudflared Access TCP forwarder")]
Tcp(CloudflaredTcpCmd),
}
#[derive(Args, Debug)]
#[command(
after_help = "Examples:\n xbp cloudflared tcp --hostname bastion.example.com\n xbp cloudflared tcp --hostname bastion.example.com --listener 127.0.0.1:2222\n xbp cloudflared tcp --hostname bastion.example.com --destination ssh.internal:22"
)]
pub struct CloudflaredTcpCmd {
#[arg(long, help = "Protected Cloudflare Access hostname")]
pub hostname: Option<String>,
#[arg(
long,
help = "Local listener address for the forwarder (default: auto-allocate 127.0.0.1:<port>)"
)]
pub listener: Option<String>,
#[arg(
long,
help = "Optional destination host:port passed to cloudflared access tcp"
)]
pub destination: Option<String>,
#[arg(long, help = "Override the cloudflared binary path")]
pub binary: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct NginxCmd {
#[command(subcommand)]
pub command: NginxSubCommand,
}
#[derive(Args, Debug)]
pub struct NetworkCmd {
#[command(subcommand)]
pub command: NetworkSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum NetworkSubCommand {
#[command(about = "Manage persistent floating IP configuration")]
FloatingIp(NetworkFloatingIpCmd),
#[command(about = "Inspect discovered network configuration sources")]
Config(NetworkConfigCmd),
#[command(about = "Manage Hetzner-specific Linux network configuration")]
Hetzner(NetworkHetznerCmd),
}
#[derive(Args, Debug)]
pub struct NetworkFloatingIpCmd {
#[command(subcommand)]
pub command: NetworkFloatingIpSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum NetworkFloatingIpSubCommand {
#[command(about = "Add a persistent floating IP entry to detected network backend")]
Add {
#[arg(long, help = "Floating IP address (IPv4 or IPv6)")]
ip: String,
#[arg(long, help = "CIDR suffix (defaults: IPv4=32, IPv6=64)")]
cidr: Option<u8>,
#[arg(long, help = "Network interface override (auto-detected when omitted)")]
interface: Option<String>,
#[arg(long, help = "Optional label for backend metadata/file naming")]
label: Option<String>,
#[arg(long, help = "Apply network changes after writing config")]
apply: bool,
#[arg(long, help = "Preview computed changes without writing files")]
dry_run: bool,
},
#[command(about = "List floating IPs from runtime and persisted network config")]
List {
#[arg(long, help = "Emit JSON output")]
json: bool,
},
}
#[derive(Args, Debug)]
pub struct NetworkConfigCmd {
#[command(subcommand)]
pub command: NetworkConfigSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum NetworkConfigSubCommand {
#[command(about = "List detected backend and configuration source files")]
List {
#[arg(long, help = "Emit JSON output")]
json: bool,
},
}
#[derive(Args, Debug)]
pub struct NetworkHetznerCmd {
#[command(subcommand)]
pub command: NetworkHetznerSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum NetworkHetznerSubCommand {
#[command(about = "Configure a Hetzner vSwitch VLAN interface persistently")]
Vswitch(NetworkHetznerVswitchCmd),
}
#[derive(Args, Debug)]
pub struct NetworkHetznerVswitchCmd {
#[command(subcommand)]
pub command: NetworkHetznerVswitchSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum NetworkHetznerVswitchSubCommand {
#[command(about = "Write persistent Linux config for a Hetzner vSwitch VLAN interface")]
Setup {
#[arg(
long,
help = "Private IPv4 address to assign on the vSwitch VLAN interface"
)]
ip: String,
#[arg(
long,
default_value_t = 24,
help = "CIDR prefix for --ip (default: 24)"
)]
cidr: u8,
#[arg(long, help = "Physical parent interface (auto-detected when omitted)")]
interface: Option<String>,
#[arg(long, help = "Hetzner vSwitch VLAN ID")]
vlan_id: u16,
#[arg(long, default_value_t = 1400, help = "Interface MTU (default: 1400)")]
mtu: u16,
#[arg(
long,
default_value = "10.0.3.1",
help = "Gateway for the routed Hetzner cloud network"
)]
gateway: String,
#[arg(
long,
default_value = "10.0.0.0/16",
help = "Destination CIDR routed through the Hetzner vSwitch gateway"
)]
route_cidr: String,
#[arg(long, help = "Apply or activate the new config immediately")]
apply: bool,
#[arg(long, help = "Preview file changes without writing them")]
dry_run: bool,
},
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
pub enum NginxDnsMode {
Manual,
Plugin,
}
#[derive(Subcommand, Debug)]
pub enum NginxSubCommand {
#[command(
about = "Provision an HTTPS NGINX reverse proxy with Certbot",
long_about = "Provision an NGINX reverse proxy, issue or reuse Let's Encrypt certificates,\n\
and write final HTTP->HTTPS redirect + TLS proxy config.\n\
\n\
Wildcard domains (for example *.example.com) require DNS-01 mode.\n\
Use --dns-mode manual for interactive TXT record prompts, or --dns-mode plugin\n\
with --dns-plugin and --dns-creds for non-interactive provider automation."
)]
Setup {
#[arg(short, long, help = "Domain name (supports wildcard: *.example.com)")]
domain: String,
#[arg(short, long, help = "Port to proxy to")]
port: u16,
#[arg(
short,
long,
help = "Email used for Let's Encrypt account registration"
)]
email: String,
#[arg(
long,
value_enum,
default_value_t = NginxDnsMode::Manual,
help = "DNS challenge mode for wildcard certificates: manual or plugin"
)]
dns_mode: NginxDnsMode,
#[arg(
long,
help = "Certbot DNS plugin name for --dns-mode plugin (for example: cloudflare)"
)]
dns_plugin: Option<String>,
#[arg(
long,
help = "Path to DNS plugin credentials file for --dns-mode plugin"
)]
dns_creds: Option<PathBuf>,
#[arg(
long,
default_value_t = true,
action = clap::ArgAction::Set,
value_parser = clap::builder::BoolishValueParser::new(),
help = "For wildcard domains, also request the base domain certificate (true|false)"
)]
include_base: bool,
},
#[command(about = "List discovered NGINX sites with listen/upstream ports")]
List,
#[command(about = "Show full NGINX config for one domain or all domains")]
Show {
#[arg(help = "Optional domain name to inspect")]
domain: Option<String>,
},
#[command(about = "Open an NGINX site config in your configured editor")]
Edit {
#[arg(help = "Domain name to edit")]
domain: String,
},
#[command(about = "Update upstream port for an existing NGINX site")]
Update {
#[arg(short, long, help = "Domain name to update")]
domain: String,
#[arg(short, long, help = "New port to proxy to")]
port: u16,
},
}
#[derive(Args, Debug)]
pub struct DiagCmd {
#[arg(long, help = "Check Nginx configuration")]
pub nginx: bool,
#[arg(long, help = "Check specific ports (comma-separated)")]
pub ports: Option<String>,
#[arg(long, help = "Skip internet speed test")]
pub no_speed_test: bool,
#[arg(
long,
help = "Path to docker compose file to validate (defaults to docker-compose.yml/compose.yml)"
)]
pub compose_file: Option<String>,
}
#[derive(Args, Debug)]
pub struct MonitorCmd {
#[command(subcommand)]
pub command: Option<MonitorSubCommand>,
}
#[derive(Subcommand, Debug)]
pub enum MonitorSubCommand {
Check,
Start,
}
#[cfg(feature = "monitoring")]
#[derive(Args, Debug)]
pub struct MonitoringCmd {
#[command(subcommand)]
pub command: MonitoringSubCommand,
}
#[cfg(feature = "monitoring")]
#[derive(Subcommand, Debug)]
pub enum MonitoringSubCommand {
Serve {
#[arg(
short,
long,
default_value = "prodzilla.yml",
help = "Monitoring config file"
)]
file: String,
},
RunOnce {
#[arg(
short,
long,
default_value = "prodzilla.yml",
help = "Monitoring config file"
)]
file: String,
#[arg(long, help = "Run probes only")]
probes_only: bool,
#[arg(long, help = "Run stories only")]
stories_only: bool,
},
List {
#[arg(
short,
long,
default_value = "prodzilla.yml",
help = "Monitoring config file"
)]
file: String,
},
}
#[derive(Args, Debug)]
#[command(
arg_required_else_help = true,
after_help = "Examples:\n xbp api install --port 8080\n xbp api health\n xbp api projects list\n xbp api daemons list\n xbp api jobs list --status queued\n xbp api routes list --base-url http://127.0.0.1:8080\n xbp api request /api/registry/installers/python-pip --web\n\nUse `--web` to target the hosted xbp.app origin instead of API_XBP_URL."
)]
pub struct ApiCmd {
#[command(subcommand)]
pub command: ApiSubCommand,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ApiTargetOptions {
#[arg(long, help = "Override the request base URL for this command")]
pub base_url: Option<String>,
#[arg(
long,
help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
)]
pub web: bool,
#[arg(
long,
help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
)]
pub no_auth: bool,
#[arg(
long,
help = "Extra header in 'Name: Value' format",
value_name = "HEADER"
)]
pub header: Vec<String>,
#[arg(long, help = "Print response headers")]
pub include_headers: bool,
#[arg(
long,
help = "Print the response body as-is without JSON pretty formatting"
)]
pub raw: bool,
}
#[cfg(feature = "docker")]
#[derive(Args, Debug)]
pub struct DockerCmd {
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Arguments to pass directly to the Docker CLI (default: --help)"
)]
pub args: Vec<String>,
}
#[derive(Subcommand, Debug)]
pub enum ApiSubCommand {
#[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
Install {
#[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
port: u16,
},
#[command(about = "Call the XBP API health endpoint")]
Health(ApiHealthCmd),
#[command(about = "Manage XBP control-plane projects")]
Projects(ApiProjectsCmd),
#[command(about = "Manage XBP daemon registrations and heartbeats")]
Daemons(ApiDaemonsCmd),
#[command(about = "Manage XBP deployment jobs")]
Jobs(ApiJobsCmd),
#[command(about = "Manage XBP proxy routes on the local API server")]
Routes(ApiRoutesCmd),
#[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
Request(ApiRequestCmd),
}
#[derive(Args, Debug)]
pub struct ApiHealthCmd {
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiProjectsCmd {
#[command(subcommand)]
pub command: ApiProjectsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum ApiProjectsSubCommand {
#[command(about = "List projects from the XBP control-plane API")]
List(ApiProjectsListCmd),
#[command(about = "Create or upsert a control-plane project")]
Create(Box<ApiProjectsCreateCmd>),
}
#[derive(Args, Debug)]
pub struct ApiProjectsListCmd {
#[arg(long, help = "Optional organization ID filter")]
pub organization_id: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiProjectsCreateCmd {
#[arg(long, help = "Project name")]
pub name: String,
#[arg(long, help = "Project path or repo path key")]
pub path: String,
#[arg(long, help = "Optional organization ID")]
pub organization_id: Option<String>,
#[arg(long, help = "Optional project slug")]
pub slug: Option<String>,
#[arg(long, help = "Optional project version")]
pub version: Option<String>,
#[arg(long, help = "Optional build directory")]
pub build_dir: Option<String>,
#[arg(long, help = "Optional runtime enum value")]
pub runtime: Option<String>,
#[arg(long, help = "Optional default branch")]
pub default_branch: Option<String>,
#[arg(long, help = "Optional repository root directory")]
pub root_directory: Option<String>,
#[arg(long, help = "Optional build command")]
pub build_command: Option<String>,
#[arg(long, help = "Optional install command")]
pub install_command: Option<String>,
#[arg(long, help = "Optional start command")]
pub start_command: Option<String>,
#[arg(long, help = "Optional output directory")]
pub output_directory: Option<String>,
#[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
pub repository_json: Option<String>,
#[arg(long, help = "Runtime policy JSON payload")]
pub runtime_policy_json: Option<String>,
#[arg(long, help = "Metadata JSON object")]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiDaemonsCmd {
#[command(subcommand)]
pub command: ApiDaemonsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum ApiDaemonsSubCommand {
#[command(about = "List registered daemons")]
List(ApiDaemonsListCmd),
#[command(about = "Register or upsert a daemon record")]
Register(ApiDaemonsRegisterCmd),
#[command(about = "Post a heartbeat update for a daemon")]
Heartbeat(ApiDaemonsHeartbeatCmd),
#[command(about = "Update daemon status only")]
UpdateStatus(ApiDaemonsUpdateStatusCmd),
}
#[derive(Args, Debug)]
pub struct ApiDaemonsListCmd {
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiDaemonsRegisterCmd {
#[arg(long, help = "Daemon node name")]
pub node_name: String,
#[arg(long, help = "Daemon hostname")]
pub hostname: String,
#[arg(long, help = "Daemon binary version")]
pub version: String,
#[arg(long, help = "Optional region")]
pub region: Option<String>,
#[arg(long, help = "Optional public IP")]
pub public_ip: Option<String>,
#[arg(long, help = "Optional internal IP")]
pub internal_ip: Option<String>,
#[arg(long, help = "Optional status enum value")]
pub status: Option<String>,
#[arg(long, help = "Optional CPU core count")]
pub cpu_cores: Option<i32>,
#[arg(long, help = "Optional total memory in MB")]
pub memory_total_mb: Option<i32>,
#[arg(long, help = "Optional total disk in GB")]
pub disk_total_gb: Option<i32>,
#[arg(long, help = "Labels JSON object")]
pub labels_json: Option<String>,
#[arg(long, help = "Metadata JSON object")]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiDaemonsHeartbeatCmd {
#[arg(help = "Daemon ID")]
pub daemon_id: String,
#[arg(long, help = "Optional status enum value")]
pub status: Option<String>,
#[arg(long, help = "Optional daemon version")]
pub version: Option<String>,
#[arg(long, help = "Optional public IP")]
pub public_ip: Option<String>,
#[arg(long, help = "Optional internal IP")]
pub internal_ip: Option<String>,
#[arg(long, help = "Optional CPU core count")]
pub cpu_cores: Option<i32>,
#[arg(long, help = "Optional total memory in MB")]
pub memory_total_mb: Option<i32>,
#[arg(long, help = "Optional total disk in GB")]
pub disk_total_gb: Option<i32>,
#[arg(long, help = "Labels JSON object")]
pub labels_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiDaemonsUpdateStatusCmd {
#[arg(help = "Daemon ID")]
pub daemon_id: String,
#[arg(long, help = "Daemon status enum value")]
pub status: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiJobsCmd {
#[command(subcommand)]
pub command: ApiJobsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum ApiJobsSubCommand {
#[command(about = "List deployment jobs")]
List(ApiJobsListCmd),
#[command(about = "Create a deployment job for a project")]
Create(ApiJobsCreateCmd),
#[command(about = "Claim the next deployment job for a daemon")]
Claim(ApiJobsClaimCmd),
#[command(about = "Update deployment job status")]
Update(ApiJobsUpdateCmd),
}
#[derive(Args, Debug)]
pub struct ApiJobsListCmd {
#[arg(long, help = "Optional project ID filter")]
pub project_id: Option<String>,
#[arg(long, help = "Optional deployment ID filter")]
pub deployment_id: Option<String>,
#[arg(long, help = "Optional daemon ID filter")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional status filter")]
pub status: Option<String>,
#[arg(long, help = "Optional result limit")]
pub limit: Option<usize>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiJobsCreateCmd {
#[arg(long, help = "Project ID")]
pub project_id: String,
#[arg(long, help = "Deployment ID")]
pub deployment_id: String,
#[arg(long, help = "Optional daemon ID assignment")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional priority")]
pub priority: Option<i32>,
#[arg(long, help = "Optional max attempts")]
pub max_attempts: Option<i32>,
#[arg(long, help = "Optional RFC3339 run-after timestamp")]
pub run_after: Option<String>,
#[arg(long, help = "Optional payload JSON object")]
pub payload_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiJobsClaimCmd {
#[arg(long, help = "Daemon ID claiming work")]
pub daemon_id: String,
#[arg(long, help = "Optional lock owner")]
pub locked_by: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiJobsUpdateCmd {
#[arg(help = "Deployment job ID")]
pub job_id: String,
#[arg(long, help = "Deployment job status enum value")]
pub status: String,
#[arg(long, help = "Optional error text")]
pub error_text: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRoutesCmd {
#[command(subcommand)]
pub command: ApiRoutesSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum ApiRoutesSubCommand {
#[command(about = "List configured proxy routes")]
List(ApiRoutesListCmd),
#[command(about = "Create or replace a proxy route")]
Create(ApiRoutesCreateCmd),
#[command(about = "Delete a proxy route by domain")]
Delete(ApiRoutesDeleteCmd),
}
#[derive(Args, Debug)]
pub struct ApiRoutesListCmd {
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRoutesCreateCmd {
#[arg(long, help = "Domain name for the route")]
pub domain: String,
#[arg(long, help = "Upstream target URL", required = true)]
pub target: Vec<String>,
#[arg(
long,
help = "Weighted upstream target in url=weight form",
value_name = "URL=WEIGHT"
)]
pub weighted_target: Vec<String>,
#[arg(long, help = "Optional header condition")]
pub header_condition: Option<String>,
#[arg(long, help = "Optional path prefix condition")]
pub path_prefix: Option<String>,
#[command(flatten)]
pub target_options: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRoutesDeleteCmd {
#[arg(help = "Domain name for the route")]
pub domain: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRequestCmd {
#[arg(help = "Request path like /projects or a full https:// URL")]
pub path: String,
#[arg(
short = 'X',
long,
help = "HTTP method to use (default: GET, or POST when a body is provided)"
)]
pub method: Option<String>,
#[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
pub body: Option<String>,
#[arg(long, help = "Read the request body from a file")]
pub body_file: Option<PathBuf>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct TailCmd {
#[arg(long, help = "Tail Kafka topic instead of log files")]
pub kafka: bool,
#[arg(long, help = "Ship logs to Kafka")]
pub ship: bool,
}
#[derive(Args, Debug)]
pub struct GenerateCmd {
#[command(subcommand)]
pub command: GenerateSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum GenerateSubCommand {
#[command(about = "Generate or update .xbp/xbp.yaml (and convert legacy JSON)")]
Config(GenerateConfigCmd),
Systemd(GenerateSystemdCmd),
}
#[derive(Args, Debug)]
pub struct GenerateConfigCmd {
#[arg(
long,
help = "Overwrite .xbp/xbp.yaml if it already exists (default errors when present)"
)]
pub force: bool,
#[arg(
long,
help = "Refresh .xbp/xbp.yaml by applying project detection defaults for missing fields"
)]
pub update: bool,
#[arg(
long,
help = "Path to a legacy xbp.json file to convert into .xbp/xbp.yaml"
)]
pub from_json: Option<PathBuf>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct SecretsCmd {
#[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
pub provider: SecretsProviderKind,
#[arg(long, help = "GitHub repository override (owner/repo)")]
pub repo: Option<String>,
#[arg(
long,
help = "Provider token override (GitHub token or Cloudflare API token)"
)]
pub token: Option<String>,
#[arg(long, help = "Cloudflare account ID override")]
pub account_id: Option<String>,
#[arg(
long = "environment",
alias = "env",
value_enum,
default_value_t = SecretsEnvironment::XbpDev,
help = "GitHub Actions environment to sync (default: xbp-dev)"
)]
pub environment: SecretsEnvironment,
#[command(subcommand)]
pub command: Option<SecretsSubCommand>,
}
#[cfg(feature = "secrets")]
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum SecretsProviderKind {
Github,
Cloudflare,
}
#[cfg(feature = "secrets")]
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum SecretsEnvironment {
#[value(name = "xbp-dev")]
XbpDev,
#[value(name = "xbp-preview")]
XbpPreview,
#[value(name = "xbp-prod")]
XbpProd,
}
#[cfg(feature = "secrets")]
impl SecretsEnvironment {
pub fn as_str(self) -> &'static str {
match self {
Self::XbpDev => "xbp-dev",
Self::XbpPreview => "xbp-preview",
Self::XbpProd => "xbp-prod",
}
}
}
#[cfg(feature = "secrets")]
#[derive(Subcommand, Debug)]
pub enum SecretsSubCommand {
#[command(alias = "ls", alias = "list-providers")]
Providers,
List(ListCmd),
Push(PushCmd),
Pull(PullCmd),
GenerateDefault(GenerateDefaultCmd),
GenerateExample(GenerateExampleCmd),
Diff,
Verify,
#[command(name = "diag", alias = "doctor")]
Diag,
Stores(SecretsStoresCmd),
Secrets(CloudflareSecretsCmd),
Quota(SecretsQuotaCmd),
#[command(name = "usage")]
Usage,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct ListCmd {
#[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
pub file: Option<String>,
#[arg(long, help = "Output format: plain (default) or json")]
pub format: Option<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct PushCmd {
#[arg(long, help = "Path to env file (default: .env.local/.env)")]
pub file: Option<String>,
#[arg(
long,
help = "Force overwrite existing GitHub Actions environment variables"
)]
pub force: bool,
#[arg(long, help = "Show what would be pushed without making changes")]
pub dry_run: bool,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct PullCmd {
#[arg(long, help = "Output file path (default: .env.local)")]
pub output: Option<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct GenerateDefaultCmd {
#[arg(long, help = "Output file path (default: .env.default)")]
pub output: Option<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct GenerateExampleCmd {
#[arg(long, help = "Output file path (default: .env.example)")]
pub output: Option<String>,
#[arg(long, help = "Remove keys from .env.local not in .env.example")]
pub clean: bool,
#[arg(long, help = "Only include vars matching prefix (repeatable)")]
pub include_prefix: Vec<String>,
#[arg(long, help = "Exclude vars matching prefix (repeatable)")]
pub exclude_prefix: Vec<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct SecretsStoresCmd {
#[command(subcommand)]
pub command: SecretsStoresSubCommand,
}
#[cfg(feature = "secrets")]
#[derive(Subcommand, Debug)]
pub enum SecretsStoresSubCommand {
List(CloudflareSecretsStoreListCmd),
Get(CloudflareSecretsStoreGetCmd),
Create(CloudflareSecretsStoreCreateCmd),
Delete(CloudflareSecretsStoreDeleteCmd),
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsStoreListCmd {}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsStoreGetCmd {
#[arg(long)]
pub store_id: String,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsStoreCreateCmd {
#[arg(long)]
pub name: String,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsStoreDeleteCmd {
#[arg(long)]
pub store_id: String,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsCmd {
#[command(subcommand)]
pub command: CloudflareSecretsSubCommand,
}
#[cfg(feature = "secrets")]
#[derive(Subcommand, Debug)]
pub enum CloudflareSecretsSubCommand {
List(CloudflareSecretsListCmd),
Get(CloudflareSecretsGetCmd),
Create(CloudflareSecretsCreateCmd),
Edit(CloudflareSecretsEditCmd),
Delete(CloudflareSecretsDeleteCmd),
#[command(name = "delete-bulk")]
DeleteBulk(CloudflareSecretsBulkDeleteCmd),
Duplicate(CloudflareSecretsDuplicateCmd),
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsListCmd {
#[arg(long)]
pub store_id: String,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsGetCmd {
#[arg(long)]
pub store_id: String,
#[arg(long)]
pub secret_id: String,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsCreateCmd {
#[arg(long)]
pub store_id: String,
#[arg(long)]
pub name: String,
#[arg(long)]
pub value: String,
#[arg(long, value_delimiter = ',')]
pub scopes: Vec<String>,
#[arg(long)]
pub comment: Option<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsEditCmd {
#[arg(long)]
pub store_id: String,
#[arg(long)]
pub secret_id: String,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub value: Option<String>,
#[arg(long, value_delimiter = ',')]
pub scopes: Vec<String>,
#[arg(long)]
pub comment: Option<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsDeleteCmd {
#[arg(long)]
pub store_id: String,
#[arg(long)]
pub secret_id: String,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsBulkDeleteCmd {
#[arg(long)]
pub store_id: String,
#[arg(long = "secret-id", required = true)]
pub secret_ids: Vec<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct CloudflareSecretsDuplicateCmd {
#[arg(long)]
pub store_id: String,
#[arg(long)]
pub secret_id: String,
#[arg(long)]
pub name: String,
#[arg(long, value_delimiter = ',')]
pub scopes: Vec<String>,
#[arg(long)]
pub comment: Option<String>,
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct SecretsQuotaCmd {
#[command(subcommand)]
pub command: SecretsQuotaSubCommand,
}
#[cfg(feature = "secrets")]
#[derive(Subcommand, Debug)]
pub enum SecretsQuotaSubCommand {
Get(SecretsQuotaGetCmd),
}
#[cfg(feature = "secrets")]
#[derive(Args, Debug)]
pub struct SecretsQuotaGetCmd {}
#[derive(Args, Debug)]
pub struct DnsCmd {
#[command(subcommand)]
pub command: DnsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsSubCommand {
#[command(alias = "ls", alias = "list")]
Providers,
Zones(DnsZonesCmd),
Records(DnsRecordsCmd),
Dnssec(DnssecCmd),
Settings(DnsSettingsCmd),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum DnsProviderKind {
Cloudflare,
Hetzner,
Vercel,
Custom,
}
#[derive(Args, Debug)]
pub struct DnsZonesCmd {
#[command(subcommand)]
pub command: DnsZonesSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsZonesSubCommand {
List(DnsZoneListCmd),
Get(DnsZoneGetCmd),
Create(DnsZoneCreateCmd),
Edit(DnsZoneEditCmd),
Delete(DnsZoneDeleteCmd),
}
#[derive(Args, Debug)]
pub struct DnsZoneListCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub account_id: Option<String>,
#[arg(long)]
pub account_name: Option<String>,
#[arg(long = "account-name-op")]
pub account_name_op: Option<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(long = "name-op")]
pub name_op: Option<String>,
#[arg(long)]
pub status: Option<String>,
#[arg(long = "type", value_delimiter = ',')]
pub zone_types: Vec<String>,
#[arg(long)]
pub r#match: Option<String>,
#[arg(long)]
pub order: Option<String>,
#[arg(long)]
pub direction: Option<String>,
#[arg(long)]
pub page: Option<u64>,
#[arg(long = "per-page")]
pub per_page: Option<u64>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsZoneGetCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsZoneCreateCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub name: String,
#[arg(long)]
pub account_id: Option<String>,
#[arg(long)]
pub jump_start: bool,
#[arg(long = "type")]
pub zone_type: Option<String>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsZoneEditCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub paused: Option<bool>,
#[arg(long = "type")]
pub zone_type: Option<String>,
#[arg(long = "vanity-name-server")]
pub vanity_name_servers: Vec<String>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsZoneDeleteCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordsCmd {
#[command(subcommand)]
pub command: DnsRecordsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsRecordsSubCommand {
List(DnsRecordListCmd),
Get(DnsRecordGetCmd),
Create(DnsRecordCreateCmd),
Replace(DnsRecordReplaceCmd),
Edit(DnsRecordEditCmd),
Delete(DnsRecordDeleteCmd),
Batch(DnsRecordBatchCmd),
Import(DnsRecordImportCmd),
Export(DnsRecordExportCmd),
}
#[derive(Args, Debug)]
pub struct DnsRecordListCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long = "type")]
pub record_type: Option<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub page: Option<u64>,
#[arg(long = "per-page")]
pub per_page: Option<u64>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordGetCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub record_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordCreateCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long = "type")]
pub record_type: String,
#[arg(long)]
pub name: String,
#[arg(long)]
pub content: String,
#[arg(long)]
pub ttl: Option<u32>,
#[arg(long)]
pub proxied: Option<bool>,
#[arg(long)]
pub priority: Option<u32>,
#[arg(long)]
pub comment: Option<String>,
#[arg(long = "tag")]
pub tags: Vec<String>,
#[arg(long = "data-json")]
pub data_json: Option<String>,
#[arg(long = "settings-json")]
pub settings_json: Option<String>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordReplaceCmd {
#[command(flatten)]
pub common: DnsRecordCreateCmd,
#[arg(long)]
pub record_id: String,
}
#[derive(Args, Debug)]
pub struct DnsRecordEditCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub record_id: String,
#[arg(long = "type")]
pub record_type: Option<String>,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub content: Option<String>,
#[arg(long)]
pub ttl: Option<u32>,
#[arg(long)]
pub proxied: Option<bool>,
#[arg(long)]
pub priority: Option<u32>,
#[arg(long)]
pub comment: Option<String>,
#[arg(long = "tag")]
pub tags: Vec<String>,
#[arg(long = "data-json")]
pub data_json: Option<String>,
#[arg(long = "settings-json")]
pub settings_json: Option<String>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordDeleteCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub record_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordBatchCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub input: PathBuf,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordImportCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub file: PathBuf,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsRecordExportCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnssecCmd {
#[command(subcommand)]
pub command: DnssecSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnssecSubCommand {
Get(DnssecGetCmd),
Edit(DnssecEditCmd),
}
#[derive(Args, Debug)]
pub struct DnssecGetCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnssecEditCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub status: Option<String>,
#[arg(long = "dnssec-multi-signer")]
pub dnssec_multi_signer: Option<bool>,
#[arg(long = "dnssec-presigned")]
pub dnssec_presigned: Option<bool>,
#[arg(long = "dnssec-use-nsec3")]
pub dnssec_use_nsec3: Option<bool>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsSettingsCmd {
#[command(subcommand)]
pub command: DnsSettingsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsSettingsSubCommand {
Get(DnsSettingsGetCmd),
Edit(DnsSettingsEditCmd),
}
#[derive(Args, Debug)]
pub struct DnsSettingsGetCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DnsSettingsEditCmd {
#[arg(long, value_enum)]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub flatten_all_cnames: Option<bool>,
#[arg(long)]
pub foundation_dns: Option<bool>,
#[arg(long)]
pub multi_provider: Option<bool>,
#[arg(long)]
pub ns_ttl: Option<u32>,
#[arg(long)]
pub secondary_overrides: Option<bool>,
#[arg(long)]
pub zone_mode: Option<String>,
#[arg(long = "reference-zone-id")]
pub reference_zone_id: Option<String>,
#[arg(long = "nameservers-type")]
pub nameservers_type: Option<String>,
#[arg(long = "nameservers-ns-set")]
pub nameservers_ns_set: Option<u32>,
#[arg(long = "soa-json")]
pub soa_json: Option<String>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
pub struct DomainsCmd {
#[arg(long, value_enum)]
pub provider: DomainsProviderKind,
#[arg(long)]
pub account_id: Option<String>,
#[arg(long)]
pub token: Option<String>,
#[command(subcommand)]
pub command: DomainsSubCommand,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum DomainsProviderKind {
Cloudflare,
}
#[derive(Subcommand, Debug)]
pub enum DomainsSubCommand {
Search(DomainsSearchCmd),
Check(DomainsCheckCmd),
List(DomainsListCmd),
}
#[derive(Args, Debug)]
pub struct DomainsSearchCmd {
#[arg(long)]
pub query: String,
#[arg(long = "extension")]
pub extensions: Vec<String>,
#[arg(long)]
pub limit: Option<usize>,
}
#[derive(Args, Debug)]
pub struct DomainsCheckCmd {
#[arg(long = "domain", required = true)]
pub domains: Vec<String>,
}
#[derive(Args, Debug)]
pub struct DomainsListCmd {}
#[derive(Args, Debug)]
pub struct GenerateSystemdCmd {
#[arg(
long,
default_value = "/etc/systemd/system",
help = "Directory where the systemd units are written"
)]
pub output_dir: PathBuf,
#[arg(long, help = "Only generate the unit for this service name")]
pub service: Option<String>,
#[arg(
long,
default_value_t = true,
help = "Also generate the xbp-api systemd unit alongside project/services"
)]
pub api: bool,
}
#[derive(Args, Debug)]
pub struct DoneCmd {
#[arg(long, help = "Root directory under which to discover git repos")]
pub root: Option<std::path::PathBuf>,
#[arg(
long,
default_value = "24 hours ago",
help = "Git --since value (e.g. '7 days ago')"
)]
pub since: String,
#[arg(short, long, help = "Output Markdown file path")]
pub output: Option<std::path::PathBuf>,
#[arg(long, help = "Skip AI summarization (OpenRouter)")]
pub no_ai: bool,
#[arg(short, long, help = "Discover repos recursively")]
pub recursive: bool,
#[arg(long, help = "Exclude repo by name (repeatable)")]
pub exclude: Vec<String>,
}
#[cfg(feature = "nordvpn")]
#[derive(Args, Debug)]
pub struct NordvpnCmd {
#[arg(
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
)]
pub args: Vec<String>,
}
#[cfg(feature = "kubernetes")]
#[derive(Args, Debug)]
pub struct KubernetesCmd {
#[command(subcommand)]
pub command: KubernetesSubCommand,
}
#[cfg(feature = "kubernetes")]
#[derive(Args, Debug)]
pub struct KubernetesAddonCmd {
#[command(subcommand)]
pub command: KubernetesAddonSubCommand,
}
#[cfg(feature = "kubernetes")]
#[derive(Subcommand, Debug)]
pub enum KubernetesAddonSubCommand {
List,
Enable {
#[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
name: String,
},
Disable {
#[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
name: String,
},
}
#[cfg(feature = "kubernetes")]
#[derive(Subcommand, Debug)]
pub enum KubernetesSubCommand {
Check {
#[arg(long, help = "Kubeconfig context to target")]
context: Option<String>,
#[arg(
long,
default_value = "default",
help = "Namespace to probe for workload readiness"
)]
namespace: String,
#[arg(long, help = "Skip live cluster calls (tooling check only)")]
offline: bool,
},
Generate {
#[arg(long, help = "Logical app name (used for resource names)")]
name: String,
#[arg(long, help = "Container image reference")]
image: String,
#[arg(long, default_value_t = 80, help = "Container port for the service")]
port: u16,
#[arg(long, default_value_t = 1, help = "Replica count")]
replicas: u16,
#[arg(
long,
default_value = "default",
help = "Namespace for generated resources"
)]
namespace: String,
#[arg(
long,
default_value = "k8s/xbp-manifest.yaml",
help = "Path to write the manifest bundle"
)]
output: String,
#[arg(long, help = "Optional ingress host (creates Ingress when set)")]
host: Option<String>,
},
Apply {
#[arg(long, help = "Path to manifest file")]
file: String,
#[arg(long, help = "Override kube context")]
context: Option<String>,
#[arg(long, help = "Override namespace")]
namespace: Option<String>,
#[arg(long, help = "Use --dry-run=server")]
dry_run: bool,
},
Status {
#[arg(long, default_value = "default", help = "Namespace to summarize")]
namespace: String,
#[arg(long, help = "Override kube context")]
context: Option<String>,
},
Addons(KubernetesAddonCmd),
DashboardToken {
#[arg(
long,
default_value = "kube-system",
help = "Namespace containing the dashboard token secret"
)]
namespace: String,
#[arg(
long,
default_value = "microk8s-dashboard-token",
help = "Secret name containing the dashboard login token"
)]
secret: String,
#[arg(long, help = "Override kube context")]
context: Option<String>,
},
ObservabilityCreds {
#[arg(
long,
default_value = "observability",
help = "Namespace containing Grafana secret"
)]
namespace: String,
#[arg(
long,
default_value = "kube-prom-stack-grafana",
help = "Grafana secret name"
)]
secret: String,
#[arg(long, help = "Override kube context")]
context: Option<String>,
},
Issuer {
#[arg(
long,
help = "Email used for Let's Encrypt account registration (required)"
)]
email: String,
#[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
name: String,
#[arg(
long,
default_value = "default",
help = "Namespace for the Issuer resource"
)]
namespace: String,
#[arg(
long,
default_value = "https://acme-v02.api.letsencrypt.org/directory",
help = "ACME server URL (production by default)"
)]
server: String,
#[arg(
long,
default_value = "letsencrypt-account-key",
help = "Secret used to store the ACME account private key"
)]
private_key_secret: String,
#[arg(
long,
default_value = "nginx",
help = "Ingress class name used for HTTP01 solving"
)]
ingress_class_name: String,
#[arg(long, help = "Override kube context")]
context: Option<String>,
#[arg(long, help = "Use --dry-run=server")]
dry_run: bool,
},
}
#[cfg(test)]
mod tests {
use super::{
Cli, CloudflareConfigAction, CloudflaredSubCommand, Commands, DnsProviderKind,
DnsSubCommand, DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand,
GenerateSubCommand, LinearConfigAction, NetworkFloatingIpSubCommand,
NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand, NetworkSubCommand, SshCmd,
};
#[cfg(feature = "secrets")]
use super::{
CloudflareSecretsSubCommand, SecretsEnvironment, SecretsProviderKind,
SecretsStoresSubCommand, SecretsSubCommand,
};
use clap::Parser;
use std::path::PathBuf;
#[test]
fn parses_network_floating_ip_add() {
let cli = Cli::parse_from([
"xbp",
"network",
"floating-ip",
"add",
"--ip",
"1.2.3.4",
"--apply",
]);
match cli.command {
Some(Commands::Network(network)) => match network.command {
NetworkSubCommand::FloatingIp(fip) => match fip.command {
NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
assert_eq!(ip, "1.2.3.4");
assert!(apply);
}
_ => panic!("expected add subcommand"),
},
_ => panic!("expected floating-ip subcommand"),
},
_ => panic!("expected network command"),
}
}
#[test]
fn parses_generate_config_update() {
let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
match cli.command {
Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
_ => panic!("expected generate config command"),
},
_ => panic!("expected generate command"),
}
}
#[test]
fn parses_commit_command_with_dry_run() {
let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
match cli.command {
Some(Commands::Commit(commit_cmd)) => {
assert!(commit_cmd.dry_run);
assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
assert_eq!(commit_cmd.model, "openai/gpt-4o-mini");
}
_ => panic!("expected commit command"),
}
}
#[test]
fn parses_linear_select_initiative_config_command() {
let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
match cli.command {
Some(Commands::Config(config_cmd)) => match config_cmd.provider {
Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
assert!(matches!(
linear_cmd.action,
LinearConfigAction::SelectInitiative
));
}
_ => panic!("expected linear config provider"),
},
_ => panic!("expected config command"),
}
}
#[test]
fn parses_ssh_command_with_cloudflared_and_key_auth() {
let cli = Cli::parse_from([
"xbp",
"ssh",
"--host",
"ssh.internal",
"--username",
"deploy",
"--private-key",
"C:/Users/floris/.ssh/id_ed25519",
"--cloudflared-hostname",
"bastion.example.com",
"--command",
"htop",
]);
let Some(Commands::Ssh(SshCmd {
ssh_host,
ssh_username,
private_key,
cloudflared_hostname,
command,
..
})) = cli.command
else {
panic!("expected shell command");
};
assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
assert_eq!(ssh_username.as_deref(), Some("deploy"));
assert_eq!(
private_key,
Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
);
assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
assert_eq!(command.as_deref(), Some("htop"));
}
#[test]
fn parses_cloudflared_tcp_command() {
let cli = Cli::parse_from([
"xbp",
"cloudflared",
"tcp",
"--hostname",
"bastion.example.com",
"--listener",
"127.0.0.1:2222",
]);
let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
panic!("expected cloudflared command");
};
match cloudflared_cmd.command {
CloudflaredSubCommand::Tcp(tcp_cmd) => {
assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
}
}
}
#[test]
fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
panic!("expected cloudflared command");
};
match cloudflared_cmd.command {
CloudflaredSubCommand::Tcp(tcp_cmd) => {
assert_eq!(tcp_cmd.hostname, None);
assert_eq!(tcp_cmd.listener, None);
}
}
}
#[test]
fn parses_version_workspace_publish_run_command() {
let cli = Cli::parse_from([
"xbp",
"version",
"workspace",
"publish",
"run",
"--repo",
"C:/Users/floris/Documents/GitHub/athena",
"--dry-run",
"--from",
"athena-s3",
]);
let Some(Commands::Version(version_cmd)) = cli.command else {
panic!("expected version command");
};
match version_cmd.command {
Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
match workspace_cmd.command {
super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
match publish_cmd.command {
super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
assert_eq!(
run_cmd.target.repo,
Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
);
assert!(!run_cmd.target.json);
assert!(run_cmd.dry_run);
assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
}
_ => panic!("expected publish run"),
}
}
_ => panic!("expected workspace publish"),
}
}
_ => panic!("expected version workspace command"),
}
}
#[test]
fn parses_commit_alias_with_push_flag() {
let cli = Cli::parse_from(["xbp", "c", "-p"]);
let Some(Commands::Commit(commit_cmd)) = cli.command else {
panic!("expected commit command");
};
assert!(commit_cmd.push);
assert!(!commit_cmd.dry_run);
}
#[test]
fn parses_version_alias_release_alias() {
let cli = Cli::parse_from(["xbp", "v", "r", "--draft"]);
let Some(Commands::Version(version_cmd)) = cli.command else {
panic!("expected version command");
};
let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
panic!("expected release subcommand");
};
assert!(release_cmd.draft);
}
#[test]
fn parses_publish_command_target_filter() {
let cli = Cli::parse_from(["xbp", "publish", "--allow-dirty", "--target", "npm"]);
let Some(Commands::Publish(publish_cmd)) = cli.command else {
panic!("expected publish command");
};
assert!(publish_cmd.allow_dirty);
assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
}
#[test]
fn parses_npm_setup_release_config_command() {
let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
let Some(Commands::Config(config_cmd)) = cli.command else {
panic!("expected config command");
};
let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
panic!("expected npm config command");
};
assert!(matches!(
registry_cmd.action,
super::RegistryConfigAction::SetupRelease
));
}
#[test]
fn parses_shell_alias_as_ssh_command() {
let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
panic!("expected ssh command through shell alias");
};
assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
}
#[test]
fn parses_api_request_command() {
let cli = Cli::parse_from([
"xbp",
"api",
"request",
"/api/registry/installers/python-pip",
"--web",
"--method",
"GET",
"--header",
"accept: application/json",
]);
let Some(Commands::Api(api_cmd)) = cli.command else {
panic!("expected api command");
};
match api_cmd.command {
super::ApiSubCommand::Request(request_cmd) => {
assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
assert!(request_cmd.target.web);
assert_eq!(request_cmd.method.as_deref(), Some("GET"));
assert_eq!(
request_cmd.target.header,
vec!["accept: application/json".to_string()]
);
}
_ => panic!("expected api request subcommand"),
}
}
#[test]
fn parses_api_projects_list_command() {
let cli = Cli::parse_from([
"xbp",
"api",
"projects",
"list",
"--organization-id",
"org_123",
]);
let Some(Commands::Api(api_cmd)) = cli.command else {
panic!("expected api command");
};
match api_cmd.command {
super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
super::ApiProjectsSubCommand::List(list_cmd) => {
assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
}
_ => panic!("expected projects list subcommand"),
},
_ => panic!("expected projects subcommand"),
}
}
#[test]
fn parses_api_routes_create_command() {
let cli = Cli::parse_from([
"xbp",
"api",
"routes",
"create",
"--domain",
"demo.local",
"--target",
"http://127.0.0.1:3000",
"--weighted-target",
"http://127.0.0.1:3001=3",
"--base-url",
"http://127.0.0.1:8080",
]);
let Some(Commands::Api(api_cmd)) = cli.command else {
panic!("expected api command");
};
match api_cmd.command {
super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
super::ApiRoutesSubCommand::Create(create_cmd) => {
assert_eq!(create_cmd.domain, "demo.local");
assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
assert_eq!(
create_cmd.weighted_target,
vec!["http://127.0.0.1:3001=3".to_string()]
);
assert_eq!(
create_cmd.target_options.base_url.as_deref(),
Some("http://127.0.0.1:8080")
);
}
_ => panic!("expected routes create subcommand"),
},
_ => panic!("expected routes subcommand"),
}
}
#[test]
fn parses_hetzner_vswitch_setup_command() {
let cli = Cli::parse_from([
"xbp",
"network",
"hetzner",
"vswitch",
"setup",
"--ip",
"10.0.3.2",
"--vlan-id",
"4000",
"--interface",
"enp0s31f6",
"--apply",
]);
let Some(Commands::Network(network_cmd)) = cli.command else {
panic!("expected network command");
};
match network_cmd.command {
NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
NetworkHetznerVswitchSubCommand::Setup {
ip,
cidr,
interface,
vlan_id,
apply,
..
} => {
assert_eq!(ip, "10.0.3.2");
assert_eq!(cidr, 24);
assert_eq!(interface.as_deref(), Some("enp0s31f6"));
assert_eq!(vlan_id, 4000);
assert!(apply);
}
},
},
_ => panic!("expected hetzner subcommand"),
}
}
#[cfg(feature = "secrets")]
#[test]
fn parses_secrets_diag_command() {
let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
match cli.command {
Some(Commands::Secrets(secrets_cmd)) => {
assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
assert!(matches!(
secrets_cmd.environment,
SecretsEnvironment::XbpDev
));
}
_ => panic!("expected secrets command"),
}
}
#[cfg(feature = "secrets")]
#[test]
fn parses_secrets_environment_override() {
let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
match cli.command {
Some(Commands::Secrets(secrets_cmd)) => {
assert!(matches!(
secrets_cmd.environment,
SecretsEnvironment::XbpProd
));
assert!(matches!(
secrets_cmd.command,
Some(SecretsSubCommand::Push(_))
));
}
_ => panic!("expected secrets command"),
}
}
#[cfg(feature = "secrets")]
#[test]
fn parses_secrets_providers_command() {
let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
match cli.command {
Some(Commands::Secrets(secrets_cmd)) => {
assert!(matches!(
secrets_cmd.command,
Some(SecretsSubCommand::Providers)
));
assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
}
_ => panic!("expected secrets command"),
}
}
#[cfg(feature = "secrets")]
#[test]
fn parses_cloudflare_secret_store_create() {
let cli = Cli::parse_from([
"xbp",
"secrets",
"--provider",
"cloudflare",
"stores",
"create",
"--name",
"prod",
]);
match cli.command {
Some(Commands::Secrets(secrets_cmd)) => {
assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
match secrets_cmd.command {
Some(SecretsSubCommand::Stores(stores_cmd)) => {
assert!(matches!(
stores_cmd.command,
SecretsStoresSubCommand::Create(_)
));
}
_ => panic!("expected stores subcommand"),
}
}
_ => panic!("expected secrets command"),
}
}
#[cfg(feature = "secrets")]
#[test]
fn parses_cloudflare_secret_duplicate() {
let cli = Cli::parse_from([
"xbp",
"secrets",
"--provider",
"cloudflare",
"secrets",
"duplicate",
"--store-id",
"store_1",
"--secret-id",
"secret_1",
"--name",
"COPY",
]);
match cli.command {
Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
assert!(matches!(
secrets_cmd.command,
CloudflareSecretsSubCommand::Duplicate(_)
));
}
_ => panic!("expected cloudflare secrets subcommand"),
},
_ => panic!("expected secrets command"),
}
}
#[test]
fn parses_dns_providers_command() {
let cli = Cli::parse_from(["xbp", "dns", "providers"]);
match cli.command {
Some(Commands::Dns(dns_cmd)) => {
assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
}
_ => panic!("expected dns command"),
}
}
#[test]
fn parses_dns_zone_list_command() {
let cli = Cli::parse_from([
"xbp",
"dns",
"zones",
"list",
"--provider",
"cloudflare",
"--account-name-op",
"contains",
"--type",
"full,partial",
]);
match cli.command {
Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
DnsZonesSubCommand::List(list_cmd) => {
assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
}
_ => panic!("expected dns zones list"),
},
_ => panic!("expected dns zones"),
},
_ => panic!("expected dns command"),
}
}
#[test]
fn parses_domains_search_command() {
let cli = Cli::parse_from([
"xbp",
"domains",
"--provider",
"cloudflare",
"search",
"--query",
"xbp",
"--extension",
"com",
]);
match cli.command {
Some(Commands::Domains(domains_cmd)) => {
assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
}
_ => panic!("expected domains command"),
}
}
#[test]
fn parses_cloudflare_config_account_id_command() {
let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
match cli.command {
Some(Commands::Config(config_cmd)) => match config_cmd.provider {
Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
assert!(matches!(
cloudflare_cmd.action,
CloudflareConfigAction::SetAccountId { .. }
));
}
_ => panic!("expected cloudflare config provider"),
},
_ => panic!("expected config command"),
}
}
}