use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = "xbp",
version,
// Custom version flag so `-v` works (clap's built-in short is only `-V`).
disable_version_flag = true,
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 = crate::cli::help_render::XBP_ROOT_HELP_TEMPLATE,
after_help = crate::cli::help_render::XBP_ROOT_AFTER_HELP
)]
pub struct Cli {
/// Print version
#[arg(
// Id must not be `version` — that collides with the `version` subcommand name.
short = 'v',
long = "version",
visible_short_alias = 'V',
action = ArgAction::Version
)]
print_version: Option<bool>,
#[arg(long, global = true, help = "Enable verbose debugging output")]
pub debug: bool,
#[arg(
long = "push",
global = true,
help = "Push after auto-committing generated changes (also honors `github.auto_push_on_commit` when omitted)"
)]
pub push: 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,
#[arg(long, help = "Print the complete alphabetical command reference")]
pub commands: 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 = "Sync with remote (stash-safe rebase when behind/diverged) and push the current branch",
long_about = "Automates the recover-and-push flow when plain git fails:\n\
• non-fast-forward push rejection\n\
• diverging branches under pull.ff=only\n\
• dirty unstaged/untracked WIP blocking rebase\n\n\
Steps: fetch → stash dirty WIP if needed → pull --rebase → push → restore stash.\n\
Does not create commits; use `xbp commit --push` to commit local changes first.",
after_help = "Examples:\n xbp push\n xbp commit --push # commit dirty files, then same sync+push path"
)]
Push,
#[command(
about = "Plan/run/verify service deploys from services[] (groups via deploy.groups)",
long_about = "Service-first deploy orchestration.\n\n\
Targets:\n\
• service name from services[]\n\
• deploy.groups.<name> (ordered multi-service)\n\
• all (every service with deploy.envs.<env>)\n\n\
Modes (flags):\n\
--plan print plan only (default when no other mode flag)\n\
--run apply providers (kubernetes, kubernetes-operator, worker, …)\n\
--verify read-only rollout/health checks\n\
--status status snapshot\n\
--promote promotion label (runs apply path)\n\
--history list recent deploy records under .xbp/deployments\n\n\
Does not use product islands like `xbp athena deploy`.",
after_help = "Examples:\n xbp deploy workers --plan\n xbp deploy workers --env production --run --yes\n xbp deploy @xylex-group/xbp-api-worker --verify\n xbp deploy all --env production --plan\n xbp deploy workers --history"
)]
Deploy(DeployCmd),
#[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",
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::commands::INSTALL_COMMAND_AFTER_HELP
)]
Install {
#[arg(short = 'l', long = "list", help = "List installable targets and exit")]
list: bool,
#[arg(
long = "force",
help = "Allow an explicitly selected install method to run even when it does not match the current OS"
)]
force: 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 commands with interactive preview/picker when no args given. Stores run history + registry under global ~/.xbp/logs/service/"
)]
Service {
#[arg(help = "Command (build/install/start/dev/pre). Omit to use interactive picker.")]
command: Option<String>,
#[arg(help = "Service name (omit for picker)")]
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 or inspect the CLI login flow against the XBP dashboard")]
Login(LoginCmd),
#[command(about = "Show the current signed-in CLI identity")]
Whoami,
#[command(
about = "Check crates.io for a newer XBP CLI release (and optionally install it)",
visible_alias = "upgrade",
after_help = "Examples:\n xbp update\n xbp update --json\n xbp update --install\n xbp update --fail-if-outdated\n xbp update --crate xbp"
)]
Update(UpdateCmd),
#[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 Cloudflare Workers secrets, Wrangler config helpers, D1 migrations, and deploy flows",
visible_alias = "worker"
)]
Workers(WorkersCmd),
#[command(about = "Run canonical Cloudflare Worker + Container workflows for an XBP project")]
Cloudflare(CloudflareCmd),
#[command(about = "Manage DNS providers, zones, records, DNSSEC, and settings")]
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),
#[command(
about = "Repair malformed Cursor process-monitor JSON exports",
visible_alias = "fix-pm-json"
)]
FixProcessMonitorJson(FixProcessMonitorJsonCmd),
#[command(about = "Upload Cursor local file history to the XBP dashboard")]
Cursor(CursorCmd),
#[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),
#[command(about = "Manage runner hosts, groups, inventory, and runner jobs")]
Runners(RunnersCmd),
#[command(about = "Built-in MCP server for AI agents (HTTP/SSE on port 1113)")]
Mcp(McpCmd),
#[command(
about = "Watch git worktree mutations, store local JSONL spools, and sync them to xbp.app",
visible_alias = "watch"
)]
WorktreeWatch(WorktreeWatchCmd),
#[cfg(feature = "linear")]
#[command(
about = "Manage Linear issues. Bare `xbp linear` opens a full-screen vim-style TUI; use list/show/create/… for scripts (feature-gated: linear)"
)]
Linear(LinearIssuesCmd),
#[command(
about = "Manage GitHub issues for the current repo. Run without a subcommand for the interactive hub",
visible_alias = "gh"
)]
Github(GithubIssuesCmd),
#[command(
about = "Scan code markers and idempotently file Linear/GitHub issues (manual only)",
visible_alias = "issue"
)]
Issues(TodosCmd),
#[command(
about = "Deprecated alias for `xbp issues`: scan TODO/FIXME markers and file issues"
)]
Todos(TodosCmd),
#[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::Push => "push",
Commands::Deploy(_) => "deploy",
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::Whoami => "whoami",
Commands::Update(_) => "update",
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::Workers(_) => "workers",
Commands::Cloudflare(_) => "cloudflare",
Commands::Dns(_) => "dns",
Commands::Domains(_) => "domains",
Commands::Done(_) => "done",
Commands::FixProcessMonitorJson(_) => "fix-process-monitor-json",
Commands::Cursor(_) => "cursor",
#[cfg(feature = "kubernetes")]
Commands::Kubernetes(_) => "kubernetes",
#[cfg(feature = "nordvpn")]
Commands::Nordvpn(_) => "nordvpn",
#[cfg(feature = "monitoring")]
Commands::Monitoring(_) => "monitoring",
Commands::Api(_) => "api",
Commands::Runners(_) => "runners",
Commands::Mcp(_) => "mcp",
Commands::WorktreeWatch(_) => "worktree-watch",
#[cfg(feature = "linear")]
Commands::Linear(_) => "linear",
Commands::Github(_) => "github",
Commands::Issues(_) => "issues",
Commands::Todos(_) => "todos",
#[cfg(feature = "docker")]
Commands::Docker(_) => "docker",
}
}
// ---------------------------------------------------------------------------
// Linear issues (`xbp linear`) — feature-gated: linear
// ---------------------------------------------------------------------------
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearIssuesCmd {
#[command(subcommand)]
pub command: Option<LinearSubCommand>,
/// Only issues assigned to you (viewer). Applies to the interactive TUI hub.
#[arg(long)]
pub me: bool,
#[arg(long, help = "Team key, name, or id")]
pub team: Option<String>,
#[arg(
long,
help = "State name or type (backlog|unstarted|started|completed|canceled)"
)]
pub state: Option<String>,
#[arg(long, help = "Assignee name, email, id, or `me` (overridden by --me)")]
pub assignee: Option<String>,
#[arg(long, short = 'q', help = "Filter by title/description substring")]
pub query: Option<String>,
#[arg(long = "label", help = "Require label name (repeatable; client-side)")]
pub labels: Vec<String>,
#[arg(
long,
help = "Include completed/canceled issues (default: open-ish only)"
)]
pub include_completed: bool,
#[arg(
long,
default_value = "updated",
help = "Sort: updated|created|priority|identifier|title|state"
)]
pub sort: String,
#[arg(long, help = "Sort ascending (default for most fields is descending)")]
pub asc: bool,
#[arg(long, default_value_t = 100, help = "Max issues to fetch for the TUI")]
pub limit: usize,
}
/// Alias used by `linear_cmd` handlers.
#[cfg(feature = "linear")]
pub type LinearCmd = LinearIssuesCmd;
#[cfg(feature = "linear")]
#[derive(Subcommand, Debug)]
pub enum LinearSubCommand {
#[command(about = "List issues")]
List(LinearListCmd),
#[command(about = "Search Linear issues by title/description/identifier")]
Search(LinearSearchCmd),
#[command(about = "Show a single issue")]
Show(LinearShowCmd),
#[command(about = "Create an issue")]
Create(LinearCreateCmd),
#[command(about = "Edit title/description/priority/state/labels/assignee")]
Edit(LinearEditCmd),
#[command(about = "Change issue status/workflow state")]
Status(LinearStatusCmd),
#[command(about = "Change issue priority (0–4 or none|urgent|high|medium|low)")]
Priority(LinearPriorityCmd),
#[command(about = "Set or multi-select labels")]
Labels(LinearLabelsCmd),
#[command(about = "Assign or unassign")]
Assign(LinearAssignCmd),
#[command(
about = "Multi-select issues and bulk assign / status / labels / project / cycle / priority"
)]
Bulk(LinearBulkCmd),
#[command(about = "Post a comment")]
Comment(LinearCommentCmd),
#[command(about = "List comments on an issue")]
Comments(LinearCommentsCmd),
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearListCmd {
#[arg(long, help = "Only issues assigned to you (viewer)")]
pub me: bool,
#[arg(long, help = "Team key, name, or id")]
pub team: Option<String>,
#[arg(
long,
help = "State name or type (backlog|unstarted|started|completed|canceled)"
)]
pub state: Option<String>,
#[arg(long, help = "Assignee name, email, id, or `me` (overridden by --me)")]
pub assignee: Option<String>,
#[arg(
long,
short = 'q',
help = "Filter by title/description substring (client-side)"
)]
pub query: Option<String>,
#[arg(long = "label", help = "Require label name (repeatable; client-side)")]
pub labels: Vec<String>,
#[arg(
long,
help = "Include completed/canceled issues (default: open-ish only)"
)]
pub include_completed: bool,
#[arg(
long,
default_value = "updated",
help = "Sort: updated|created|priority|identifier|title|state"
)]
pub sort: String,
#[arg(long, help = "Sort ascending")]
pub asc: bool,
#[arg(long, default_value_t = 50, help = "Max issues to fetch")]
pub limit: usize,
#[arg(
long,
help = "Open the full-screen Linear TUI instead of a plain table"
)]
pub tui: bool,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearSearchCmd {
#[arg(help = "Title, description, or identifier substring")]
pub query: String,
#[arg(long, help = "Only issues assigned to you (viewer)")]
pub me: bool,
#[arg(long, help = "Team key, name, or id")]
pub team: Option<String>,
#[arg(
long,
help = "State name or type (backlog|unstarted|started|completed|canceled)"
)]
pub state: Option<String>,
#[arg(long, help = "Assignee name, email, id, or `me` (overridden by --me)")]
pub assignee: Option<String>,
#[arg(long = "label", help = "Require label name (repeatable; client-side)")]
pub labels: Vec<String>,
#[arg(
long,
help = "Include completed/canceled issues (default: open-ish only)"
)]
pub include_completed: bool,
#[arg(
long,
default_value = "updated",
help = "Sort: updated|created|priority|identifier|title|state"
)]
pub sort: String,
#[arg(long, help = "Sort ascending")]
pub asc: bool,
#[arg(long, default_value_t = 50, help = "Max issues to fetch")]
pub limit: usize,
#[arg(
long,
help = "Open the full-screen Linear TUI with the search preloaded"
)]
pub tui: bool,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearShowCmd {
#[arg(help = "Issue id or identifier (e.g. XLX-29)")]
pub id: String,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearCreateCmd {
#[arg(long, help = "Issue title")]
pub title: Option<String>,
#[arg(long, help = "Team key, name, or id")]
pub team: Option<String>,
#[arg(long, help = "Markdown description")]
pub description: Option<String>,
#[arg(long, help = "Priority 0–4 or none|urgent|high|medium|low")]
pub priority: Option<String>,
#[arg(long, help = "Initial state name or id")]
pub state: Option<String>,
#[arg(long, help = "Assignee name, email, id, or `me`")]
pub assignee: Option<String>,
#[arg(long = "label", help = "Label name or id (repeatable)")]
pub labels: Vec<String>,
#[arg(long, help = "Prompt for description when interactive")]
pub interactive_body: bool,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearEditCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub description: Option<String>,
#[arg(long)]
pub priority: Option<String>,
#[arg(long)]
pub state: Option<String>,
#[arg(long)]
pub assignee: Option<String>,
#[arg(long = "label")]
pub labels: Vec<String>,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearStatusCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
#[arg(long, help = "State name, type, or id")]
pub state: Option<String>,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearPriorityCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
#[arg(long, help = "Priority 0–4 or none|urgent|high|medium|low")]
pub priority: Option<String>,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearLabelsCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
#[arg(long, help = "Label to add (repeatable)")]
pub add: Vec<String>,
#[arg(long, help = "Label to remove (repeatable)")]
pub remove: Vec<String>,
#[arg(long, help = "Replace all labels (repeatable)")]
pub set: Vec<String>,
#[arg(long, help = "Clear all labels")]
pub clear: bool,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearAssignCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
#[arg(long, help = "Assignee name/email/id/`me`/`none`")]
pub assignee: Option<String>,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearBulkCmd {
/// Explicit issue ids/identifiers (repeatable). Skips the multi-select picker when set.
#[arg(long = "id", help = "Issue id or identifier (repeatable)")]
pub ids: Vec<String>,
#[arg(long, help = "Only issues assigned to you (viewer)")]
pub me: bool,
#[arg(long, help = "Team key, name, or id (candidate filter)")]
pub team: Option<String>,
#[arg(long, help = "State name or type filter for candidates")]
pub state: Option<String>,
#[arg(long, help = "Assignee filter for candidates (`me`/name/email/id)")]
pub assignee: Option<String>,
#[arg(long, short = 'q', help = "Title/description substring filter")]
pub query: Option<String>,
#[arg(long = "label", help = "Require label on candidates (repeatable)")]
pub filter_labels: Vec<String>,
#[arg(long, help = "Include completed/canceled issues in the candidate list")]
pub include_completed: bool,
#[arg(
long,
default_value = "updated",
help = "Sort: updated|created|priority|identifier|title|state"
)]
pub sort: String,
#[arg(long, help = "Sort ascending")]
pub asc: bool,
#[arg(long, default_value_t = 100, help = "Max candidate issues to fetch")]
pub limit: usize,
#[arg(long, help = "Bulk-assign to this user (`me`/`none`/name/email/id)")]
pub assign: Option<String>,
#[arg(long = "set-state", help = "Bulk set workflow state name/type/id")]
pub set_state: Option<String>,
#[arg(
long = "add-label",
help = "Label to add to all selected issues (repeatable)"
)]
pub add_labels: Vec<String>,
#[arg(long, help = "Project name/id (`none` to clear)")]
pub project: Option<String>,
#[arg(long, help = "Cycle name/number/id (`none` to clear; one team only)")]
pub cycle: Option<String>,
#[arg(long, help = "Priority 0–4 or none|urgent|high|medium|low")]
pub priority: Option<String>,
#[arg(long, short = 'y', help = "Skip confirmation")]
pub yes: bool,
#[arg(long, help = "Show plan only; do not write")]
pub dry_run: bool,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearCommentCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
#[arg(long, short = 'm', help = "Comment body (markdown)")]
pub body: Option<String>,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearCommentsCmd {
#[arg(help = "Issue id or identifier")]
pub id: String,
}
// ---------------------------------------------------------------------------
// GitHub issues (`xbp github`)
// ---------------------------------------------------------------------------
#[derive(Args, Debug)]
pub struct GithubIssuesCmd {
#[arg(long, help = "Repository owner (default: git origin)")]
pub owner: Option<String>,
#[arg(long, help = "Repository name (default: git origin)")]
pub repo: Option<String>,
#[command(subcommand)]
pub command: Option<GithubSubCommand>,
}
/// Alias used by `github_cmd` handlers.
pub type GithubCmd = GithubIssuesCmd;
#[derive(Subcommand, Debug)]
pub enum GithubSubCommand {
#[command(about = "List issues")]
List(GithubListCmd),
#[command(about = "Show a single issue")]
Show(GithubShowCmd),
#[command(about = "Create an issue")]
Create(GithubCreateCmd),
#[command(about = "Edit title/body/labels")]
Edit(GithubEditCmd),
#[command(about = "Open or close an issue")]
Status(GithubStatusCmd),
#[command(about = "Set labels")]
Labels(GithubLabelsCmd),
#[command(about = "Assign or unassign")]
Assign(GithubAssignCmd),
#[command(about = "Post a comment")]
Comment(GithubCommentCmd),
#[command(about = "List comments")]
Comments(GithubCommentsCmd),
}
#[derive(Args, Debug)]
pub struct GithubListCmd {
#[arg(long, default_value = "open", help = "open | closed | all")]
pub state: String,
#[arg(long = "label", help = "Label filter (repeatable)")]
pub labels: Vec<String>,
#[arg(long, help = "Assignee login or `none`")]
pub assignee: Option<String>,
#[arg(long, short = 'q', help = "Client-side title/body filter")]
pub query: Option<String>,
#[arg(long, default_value_t = 50)]
pub limit: usize,
}
#[derive(Args, Debug)]
pub struct GithubShowCmd {
#[arg(help = "Issue number or URL")]
pub id: String,
}
#[derive(Args, Debug)]
pub struct GithubCreateCmd {
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub body: Option<String>,
#[arg(long = "label")]
pub labels: Vec<String>,
#[arg(long = "assignee")]
pub assignees: Vec<String>,
#[arg(long, help = "Prompt for body when interactive")]
pub interactive_body: bool,
}
#[derive(Args, Debug)]
pub struct GithubEditCmd {
#[arg(help = "Issue number")]
pub id: String,
#[arg(long)]
pub title: Option<String>,
#[arg(long)]
pub body: Option<String>,
#[arg(long = "label")]
pub labels: Vec<String>,
}
#[derive(Args, Debug)]
pub struct GithubStatusCmd {
#[arg(help = "Issue number")]
pub id: String,
#[arg(long, help = "open or closed")]
pub state: Option<String>,
}
#[derive(Args, Debug)]
pub struct GithubLabelsCmd {
#[arg(help = "Issue number")]
pub id: String,
#[arg(long)]
pub add: Vec<String>,
#[arg(long)]
pub remove: Vec<String>,
#[arg(long)]
pub set: Vec<String>,
#[arg(long)]
pub clear: bool,
}
#[derive(Args, Debug)]
pub struct GithubAssignCmd {
#[arg(help = "Issue number")]
pub id: String,
#[arg(long, help = "GitHub login, or `none` to unassign")]
pub assignee: Option<String>,
}
#[derive(Args, Debug)]
pub struct GithubCommentCmd {
#[arg(help = "Issue number")]
pub id: String,
#[arg(long, short = 'm')]
pub body: Option<String>,
}
#[derive(Args, Debug)]
pub struct GithubCommentsCmd {
#[arg(help = "Issue number")]
pub id: String,
}
// ---------------------------------------------------------------------------
// Issue marker automation (`xbp issues`, deprecated alias `xbp todos`)
// ---------------------------------------------------------------------------
#[derive(Args, Debug)]
pub struct TodosCmd {
#[command(subcommand)]
pub command: Option<TodosSubCommand>,
}
#[derive(Subcommand, Debug)]
pub enum TodosSubCommand {
#[command(about = "Scan the codebase for TODO/FIXME markers (default)")]
Scan(TodosScanCmd),
#[command(about = "Idempotently create Linear and/or GitHub issues from code markers")]
Sync(TodosSyncCmd),
#[command(about = "Show per-marker linkage table + ledger summary")]
Status(TodosStatusCmd),
#[command(about = "Remove ledger entries for markers no longer present in code")]
Prune(TodosPruneCmd),
#[command(
about = "Measure coding time per linked issue from worktree-watch edits/commits on tracked paths"
)]
Effort(TodosEffortCmd),
#[command(
about = "Mark ledger issues done when Linear/GitHub report completed/closed (stops time accumulation)"
)]
Reconcile(TodosReconcileCmd),
#[command(about = "Search Linear and/or GitHub issues")]
Search(IssueSearchCmd),
#[command(about = "Interactive wizard: write issues automation into .xbp/xbp.yaml")]
Setup,
}
#[derive(Args, Debug)]
pub struct IssueSearchCmd {
#[arg(help = "Title/body/identifier substring")]
pub query: String,
#[arg(long, help = "Search GitHub issues")]
pub github: bool,
#[cfg(feature = "linear")]
#[arg(long, help = "Search Linear issues")]
pub linear: bool,
#[arg(long, help = "GitHub owner override")]
pub owner: Option<String>,
#[arg(long, help = "GitHub repo override")]
pub repo: Option<String>,
#[arg(long, help = "State filter")]
pub state: Option<String>,
#[arg(long = "label", help = "Require label name (repeatable; client-side)")]
pub labels: Vec<String>,
#[arg(long, help = "Assignee filter (`me` supported for Linear)")]
pub assignee: Option<String>,
#[arg(long, help = "Include completed/canceled/closed issues")]
pub include_completed: bool,
#[arg(
long,
default_value = "updated",
help = "Linear sort: updated|created|priority|identifier|title|state"
)]
pub sort: String,
#[arg(long, help = "Sort ascending")]
pub asc: bool,
#[arg(long, default_value_t = 50, help = "Max issues per provider")]
pub limit: usize,
#[cfg(feature = "linear")]
#[arg(long, help = "Open Linear results in the Linear TUI")]
pub tui: bool,
}
#[derive(Args, Debug)]
pub struct TodosScanCmd {
#[arg(long, help = "Root path to scan (default: project/git root)")]
pub path: Option<PathBuf>,
#[arg(long, help = "Emit JSON")]
pub json: bool,
#[arg(
long,
help = "Do not offer interactive sync after scan (also set issues.prompt_sync_after_scan: false)"
)]
pub no_prompt: bool,
}
#[derive(Args, Debug)]
pub struct TodosSyncCmd {
#[arg(
long,
value_enum,
help = "Where to create issues (default: issues.default_to from config, else both)"
)]
pub to: Option<TodosSyncTarget>,
#[arg(long, help = "Root path to scan")]
pub path: Option<PathBuf>,
#[arg(long, help = "Preview creates without writing")]
pub dry_run: bool,
#[arg(
long,
short = 'y',
help = "Skip interactive multi-select; file all new markers (or set issues.auto_yes: true)"
)]
pub yes: bool,
#[cfg(feature = "linear")]
#[arg(long, help = "Linear team key/name/id (requires --features linear)")]
pub team: Option<String>,
#[arg(long, help = "GitHub owner override")]
pub owner: Option<String>,
#[arg(long, help = "GitHub repo override")]
pub repo: Option<String>,
#[arg(
long,
help = "Stamp source marker lines with created issue IDs (or set issues.annotate_source: true)"
)]
pub annotate: bool,
#[arg(
long,
help = "Do not stamp source lines even if config enables annotate_source"
)]
pub no_annotate: bool,
#[arg(
long,
help = "Opt-in: enrich issue title/body with OpenRouter (overrides issues.openrouter_enrich: false)"
)]
pub enrich: bool,
#[arg(
long,
help = "Disable OpenRouter enrichment even if todos.openrouter_enrich is true"
)]
pub no_enrich: bool,
}
#[derive(Args, Debug)]
pub struct TodosStatusCmd {
#[arg(long)]
pub path: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct TodosPruneCmd {
#[arg(long)]
pub path: Option<PathBuf>,
#[arg(long, help = "List stale entries without removing them")]
pub dry_run: bool,
}
#[derive(Args, Debug)]
pub struct TodosEffortCmd {
#[arg(long, help = "Project root (default: .xbp / git root)")]
pub path: Option<PathBuf>,
#[arg(
long,
default_value_t = 15,
help = "Max minutes between file mutations counted as one coding session (worktree-watch gap)"
)]
pub gap_minutes: u64,
#[arg(long, help = "Emit JSON report")]
pub json: bool,
#[arg(long, help = "Do not write ledger/effort snapshot")]
pub no_persist: bool,
}
#[derive(Args, Debug)]
pub struct TodosReconcileCmd {
#[arg(long, help = "Project root (default: .xbp / git root)")]
pub path: Option<PathBuf>,
#[arg(long, help = "Detect closed issues without writing the ledger")]
pub dry_run: bool,
}
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum TodosSyncTarget {
#[cfg(feature = "linear")]
Linear,
Github,
#[cfg(feature = "linear")]
Both,
}
#[derive(Args, Debug)]
#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
pub struct DeployCmd {
#[arg(help = "Service name, deploy.groups name, or `all`")]
pub target: String,
#[arg(long, help = "Deploy environment (default: deploy.default_env or production)")]
pub env: Option<String>,
#[arg(long, help = "Print plan only (no mutations). Default when no other mode is set.")]
pub plan: bool,
#[arg(long, help = "Apply deploy providers for the resolved services")]
pub run: bool,
#[arg(long, help = "Read-only verification (rollout/health/image checks)")]
pub verify: bool,
#[arg(long, help = "Status snapshot for resolved services")]
pub status: bool,
#[arg(
long,
value_name = "STAGE",
help = "Promote stage label and run apply path (e.g. stable)"
)]
pub promote: Option<String>,
#[arg(long, help = "List recent deploy history records")]
pub history: bool,
#[arg(long, help = "Skip kubernetes context confirmation prompts")]
pub yes: bool,
#[arg(long, help = "Emit plan JSON (with --plan)")]
pub json: bool,
#[arg(long, default_value_t = 20, help = "History entries to show with --history")]
pub limit: usize,
}
#[derive(Args, Debug)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::COMMIT_AFTER_HELP
)]
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,
help = "OpenRouter model override used for commit generation; otherwise XBP uses the global config default"
)]
pub model: Option<String>,
#[arg(
long,
help = "Force the conventional commit scope (for example: cli, api, docs)"
)]
pub scope: Option<String>,
}
#[derive(Args, Debug)]
#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
pub struct PortsCmd {
#[arg(short = 'p', long = "port")]
pub port: Option<u16>,
#[arg(
long = "sort",
default_value = "port",
value_parser = ["port", "pid"],
help = "Sort port rows by port or pid (default: port)"
)]
pub sort: String,
#[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)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::CONFIG_AFTER_HELP
)]
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(ValueEnum, Clone, Debug)]
pub enum ConfigFileFormat {
Json,
Jsonc,
Toml,
Yaml,
}
#[derive(Args, Debug)]
pub struct MigrateConfigFileCmd {
#[arg(value_enum, help = "Destination format")]
pub format: ConfigFileFormat,
#[arg(
long,
help = "Source XBP config path; defaults to the repository-bound config"
)]
pub from: Option<PathBuf>,
#[arg(long, help = "Destination path; defaults to .xbp/xbp.<format>")]
pub output: Option<PathBuf>,
}
#[derive(Subcommand, Debug)]
pub enum ConfigProviderCmd {
#[command(
name = "migrate-config-file",
about = "Convert the repository-bound XBP config to JSON, JSONC, TOML, or YAML"
)]
MigrateConfigFile(MigrateConfigFileCmd),
#[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 (run without a subcommand for the interactive setup wizard)"
)]
Cloudflare(CloudflareConfigCmd),
#[cfg(feature = "linear")]
#[command(
about = "Manage the Linear API key used for release-note issue linking and initiative publishing (feature-gated: linear)"
)]
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(CratesConfigCmd),
#[command(about = "Manage guided release config in .xbp/xbp.yaml")]
Release(ReleaseConfigCmd),
#[command(
about = "Interactively manage static OpenAPI generation in .xbp/xbp.yaml (run without a subcommand for the setup wizard)"
)]
Openapi(OpenapiConfigCmd),
}
#[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: Option<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,
},
#[command(about = "Interactive dashboard OAuth linking flow for Cloudflare credentials")]
Login,
#[command(about = "Show Cloudflare credential sources and readiness")]
Status,
#[command(about = "Run the interactive Cloudflare credential setup wizard")]
Setup,
}
#[cfg(feature = "linear")]
#[derive(Args, Debug)]
pub struct LinearConfigCmd {
#[command(subcommand)]
pub action: LinearConfigAction,
}
#[cfg(feature = "linear")]
#[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(Args, Debug)]
pub struct CratesConfigCmd {
#[command(subcommand)]
pub action: CratesConfigAction,
}
#[derive(Args, Debug)]
pub struct ReleaseConfigCmd {
#[command(subcommand)]
pub action: ReleaseConfigAction,
}
#[derive(Args, Debug)]
pub struct OpenapiConfigCmd {
#[command(subcommand)]
pub action: Option<OpenapiConfigAction>,
}
#[derive(Subcommand, Debug)]
pub enum OpenapiConfigAction {
#[command(about = "Run the interactive OpenAPI setup wizard for .xbp/xbp.yaml")]
Setup,
#[command(about = "Show project and per-service OpenAPI generation settings")]
Show,
}
#[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(Subcommand, Debug)]
pub enum CratesConfigAction {
#[command(about = "Set crates.io token (omit value to enter it securely)")]
SetKey {
#[arg(help = "crates.io token value")]
key: Option<String>,
},
#[command(about = "Delete the stored crates.io token from global XBP config")]
DeleteKey,
#[command(about = "Show whether a crates.io 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,
#[command(
about = "Run `cargo login` using the stored crates token and sync Cargo's local credentials file"
)]
Login {
#[arg(help = "Optional crates.io token value to save before logging in")]
key: Option<String>,
},
#[command(
about = "Run `cargo logout` to remove Cargo's local crates.io credentials while keeping XBP's stored token"
)]
Logout,
}
#[derive(Subcommand, Debug)]
pub enum ReleaseConfigAction {
#[command(
name = "setup",
about = "Interactively configure release tag naming in .xbp/xbp.yaml"
)]
Setup,
}
#[derive(Args, Debug)]
#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
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(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::LOGIN_AFTER_HELP
)]
pub struct LoginCmd {
#[command(subcommand)]
pub action: Option<LoginSubCommand>,
}
#[derive(Subcommand, Debug)]
pub enum LoginSubCommand {
#[command(about = "Show whether the current CLI session is still valid")]
Status,
#[command(about = "Revoke the current CLI token and clear local login state")]
Logout,
}
#[derive(Args, Debug)]
#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
pub struct UpdateCmd {
#[arg(
long = "crate",
default_value = "xbp",
help = "crates.io package name to check (default: xbp)"
)]
pub crate_name: String,
#[arg(long, help = "Print the update check result as JSON")]
pub json: bool,
#[arg(
long,
help = "Run `cargo install <crate> --locked --force` when a newer release is available"
)]
pub install: bool,
#[arg(
long,
help = "Exit with an error when the installed CLI is older than crates.io"
)]
pub fail_if_outdated: bool,
}
#[derive(Args, Debug)]
#[command(
subcommand_precedence_over_arg = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::VERSION_AFTER_HELP
)]
pub struct VersionCmd {
#[arg(
short = 'p',
long,
help = "Push after auto-committing version file updates"
)]
pub push: bool,
#[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),
#[command(
about = "Manage explicit release-domain ownership, validation, sync, diagnosis, and guarded releases",
arg_required_else_help = true
)]
Domain(VersionDomainCmd),
/// Discover package roots (Cargo.toml, package.json, etc.) and register them as services
#[command(name = "discover", alias = "register", alias = "register-services")]
Discover(VersionDiscoverServicesCmd),
#[command(
about = "Bump versions only for packages with uncommitted changes in the working tree"
)]
Bump(VersionBumpCmd),
}
#[derive(Args, Debug)]
pub struct VersionBumpCmd {
#[arg(
short = 'p',
long,
help = "Push after auto-committing bumped version files"
)]
pub push: bool,
#[arg(long, help = "Preview bump selections without writing version files")]
pub dry_run: bool,
#[arg(
long,
group = "bump_kind",
help = "Default bump kind for --all and interactive default selection"
)]
pub major: bool,
#[arg(
long,
group = "bump_kind",
help = "Default bump kind for --all and interactive default"
)]
pub minor: bool,
#[arg(
long,
group = "bump_kind",
help = "Default bump kind for --all and interactive default"
)]
pub patch: bool,
#[arg(
long,
help = "Bump every mutated package with the selected default kind without prompting"
)]
pub all: bool,
}
#[derive(Args, Debug)]
pub struct VersionDiscoverServicesCmd {
#[arg(
long,
help = "Preview discovered nested XBP services without writing .xbp/xbp.yaml"
)]
pub dry_run: bool,
#[arg(
long,
help = "Discover nested services only; do not register them in the root config (opt out)"
)]
pub no_register: bool,
}
#[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 = "flag",
value_enum,
help = "Append build metadata to the release version: dev, stable, beta, alpha, nightly, or exp"
)]
pub flag: Option<VersionReleaseFlag>,
#[arg(
long,
help = "Allow releasing with uncommitted changes in the working tree"
)]
pub allow_dirty: bool,
#[arg(
long,
help = "Release title (defaults to <version>[-<flag>]-<service>)"
)]
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,
requires = "publish",
help = "When `--publish` is enabled: skip configured preflight commands, allow a dirty working tree, and auto-sync workspace crate versions. Version release also does not fail on deploy-only config issues (e.g. duplicate service ports)."
)]
pub force: bool,
#[arg(
long,
help = "Plan the release (and optional package publish) without writing versions, tags, ledgers, or publishing"
)]
pub dry_run: 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(Copy, Clone, Debug, ValueEnum, PartialEq, Eq)]
pub enum VersionReleaseFlag {
Dev,
Stable,
Beta,
Alpha,
Nightly,
Exp,
}
impl VersionReleaseFlag {
pub fn as_str(self) -> &'static str {
match self {
Self::Dev => "dev",
Self::Stable => "stable",
Self::Beta => "beta",
Self::Alpha => "alpha",
Self::Nightly => "nightly",
Self::Exp => "exp",
}
}
}
#[derive(Args, Debug)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::PUBLISH_AFTER_HELP
)]
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 = "Skip configured preflight commands and publish anyway")]
pub force: bool,
#[arg(
long,
help = "When publishing a crate workspace member, also publish missing internal prerequisites in dependency order"
)]
pub include_prereqs: bool,
#[arg(long, help = "Limit publishing to one target: npm or crates")]
pub target: Option<String>,
#[arg(
long,
help = "Publish the npm package or crate belonging to this configured service"
)]
pub service: Option<String>,
#[arg(
long,
help = "Limit publishing to the workflow whose manifest matches this path"
)]
pub manifest_path: Option<PathBuf>,
}
#[derive(Args, Debug)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
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 plan --only athena-auth --include-prereqs\n xbp version workspace publish heal --only xbp --include-prereqs --write\n xbp version workspace publish run --dry-run\n xbp version workspace publish run --from athena-s3\n xbp version workspace publish run --only athena-auth --include-prereqs"
)]
pub struct VersionWorkspaceCmd {
#[command(subcommand)]
pub command: VersionWorkspaceSubCommand,
}
#[derive(Args, Debug)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = "Examples:\n xbp version domain init --name auth --root services/auth --write\n xbp version domain doctor --domain auth\n xbp version domain sync --domain auth --check\n xbp version domain diagnose --domain auth --log build.log\n xbp version domain release --domain auth --patch --deploy"
)]
pub struct VersionDomainCmd {
#[command(subcommand)]
pub command: VersionDomainSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum VersionDomainSubCommand {
#[command(about = "Discover or scaffold a version-domain policy")]
Init(VersionDomainInitCmd),
#[command(about = "Validate release-domain ownership and configured surfaces")]
Doctor(VersionDomainDoctorCmd),
#[command(about = "Preview or apply version-domain surface synchronization")]
Sync(VersionDomainSyncCmd),
#[command(about = "Classify build logs for release-domain drift and unsafe alignments")]
Diagnose(VersionDomainDiagnoseCmd),
#[command(about = "Run a guarded domain version bump/set and optional deploy")]
Release(VersionDomainReleaseCmd),
}
#[derive(Args, Debug)]
pub struct VersionDomainInitCmd {
#[arg(long, help = "Release domain name")]
pub name: String,
#[arg(long, help = "Domain root path relative to the XBP project root")]
pub root: PathBuf,
#[arg(long, help = "Persist the generated domain to .xbp/xbp.yaml")]
pub write: bool,
}
#[derive(Args, Debug)]
pub struct VersionDomainDoctorCmd {
#[arg(long = "domain", help = "Release domain name")]
pub domain: String,
}
#[derive(Args, Debug)]
pub struct VersionDomainSyncCmd {
#[arg(long = "domain", help = "Release domain name")]
pub domain: String,
#[arg(
long,
conflicts_with = "write",
help = "Validate only; do not write files"
)]
pub check: bool,
#[arg(long, conflicts_with = "check", help = "Write configured surfaces")]
pub write: bool,
}
#[derive(Args, Debug)]
pub struct VersionDomainDiagnoseCmd {
#[arg(long = "domain", help = "Release domain name")]
pub domain: String,
#[arg(long, help = "Build or deploy log file to classify")]
pub log: Option<PathBuf>,
}
#[derive(Args, Debug)]
#[command(group(
clap::ArgGroup::new("domain_release_version")
.required(true)
.args(["patch", "minor", "major", "version"])
))]
pub struct VersionDomainReleaseCmd {
#[arg(long = "domain", help = "Release domain name")]
pub domain: String,
#[arg(long, help = "Bump the domain patch version")]
pub patch: bool,
#[arg(long, help = "Bump the domain minor version")]
pub minor: bool,
#[arg(long, help = "Bump the domain major version")]
pub major: bool,
#[arg(long, help = "Set an explicit domain version")]
pub version: Option<String>,
#[arg(long, help = "Run the configured Cloudflare app release after sync")]
pub deploy: bool,
#[arg(long, help = "Allow an otherwise blocked major-version jump")]
pub allow_major_jump: bool,
#[arg(
long,
value_name = "DOMAIN",
help = "Explicitly record an allowed cross-domain version source"
)]
pub allow_cross_domain_version: Option<String>,
}
#[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 = "Inspect or auto-fix Cargo publish surface issues (publish=false, version pins, metadata)"
)]
Heal(VersionWorkspacePublishHealCmd),
#[command(about = "Publish workspace packages in dependency order")]
Run(VersionWorkspacePublishRunCmd),
}
#[derive(Args, Debug)]
pub struct VersionWorkspacePublishHealCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
#[arg(long, help = "Limit healing to the publish closure of one package")]
pub only: Option<String>,
#[arg(
long,
help = "When healing a single package, also include internal path-dependency prerequisites"
)]
pub include_prereqs: bool,
#[arg(long, help = "Write Cargo.toml fixes (default is dry preview)")]
pub write: bool,
#[arg(
long,
help = "Expected workspace version for pin alignment (default: highest package version / config)"
)]
pub version: Option<String>,
}
#[derive(Args, Debug)]
pub struct VersionWorkspacePublishPlanCmd {
#[command(flatten)]
pub target: VersionWorkspaceTargetArgs,
#[arg(long, help = "Limit the plan to one package")]
pub only: Option<String>,
#[arg(
long,
help = "When planning a single package, also include missing internal prerequisites"
)]
pub include_prereqs: bool,
}
#[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 = "When publishing one package, also publish missing internal prerequisites in dependency order"
)]
pub include_prereqs: bool,
#[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 = true,
action = clap::ArgAction::Set,
help = "Auto-fix publish-surface issues (publish=false, missing version pins) before publishing"
)]
pub auto_fix: 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)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::LOGS_AFTER_HELP
)]
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)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::SSH_AFTER_HELP
)]
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,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
)]
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(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
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)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::NGINX_AFTER_HELP
)]
pub struct NginxCmd {
#[command(subcommand)]
pub command: NginxSubCommand,
}
#[derive(Args, Debug)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
)]
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)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::DIAG_AFTER_HELP
)]
pub struct DiagCmd {
#[arg(long, help = "Check Nginx configuration")]
pub nginx: bool,
#[arg(long, hide = true)]
pub refresh_system_inventory: bool,
#[arg(
long,
help = "Refresh and print persisted machine inventory in the global XBP config"
)]
pub codetime: bool,
#[arg(
long,
help = "Inspect Cursor roaming data on Windows; implies --codetime"
)]
pub cursor: 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,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
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)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = "Examples:\n xbp runners orgs list\n xbp runners groups list --organization-id <uuid>\n xbp runners groups create --organization-id <uuid> --name builders --visibility selected\n xbp runners groups access <runner-group-id>\n xbp runners inventory --organization-id <uuid>\n xbp runners hosts enroll --organization-id <uuid> --platform linux --hostname runner-1 --runner-name-prefix xbp-linux\n xbp runners hosts preflight --runner-host-id <uuid>\n xbp runners status --organization-id <uuid>\n xbp runners deploy --organization-id <uuid> --runner-host-id <uuid>\n xbp runners agent serve --runner-host-id <uuid>"
)]
pub struct RunnersCmd {
#[command(subcommand)]
pub command: RunnersSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum RunnersSubCommand {
#[command(about = "List organizations already registered in the XBP control plane")]
OrgsList(RunnersOrgsListCmd),
#[command(about = "Manage runner groups")]
Groups(RunnersGroupsCmd),
#[command(about = "List or enroll runner hosts")]
Hosts(RunnersHostsCmd),
#[command(about = "List runner inventory for an organization")]
Inventory(RunnersInventoryCmd),
#[command(about = "Show runner status for an organization or host")]
Status(RunnersStatusCmd),
#[command(about = "Show recent runner job logs and failures")]
Logs(RunnersLogsCmd),
#[command(about = "Queue a runner sync job")]
Sync(RunnersSyncCmd),
#[command(about = "Queue a runner deployment job")]
Deploy(RunnersDeployCmd),
#[command(about = "Queue a runner update job")]
Update(RunnersUpdateCmd),
#[command(about = "Queue a runner removal job")]
Remove(RunnersRemoveCmd),
#[command(about = "Run a local runner host poller that claims and executes jobs")]
Agent(RunnersAgentCmd),
}
#[derive(Args, Debug)]
pub struct RunnersOrgsListCmd {
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersGroupsCmd {
#[command(subcommand)]
pub command: RunnersGroupsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum RunnersGroupsSubCommand {
#[command(about = "List runner groups for an organization")]
List(RunnersGroupsListCmd),
#[command(about = "Create or upsert a runner group")]
Create(RunnersGroupsCreateCmd),
#[command(about = "Update an existing runner group")]
Update(RunnersGroupsCreateCmd),
#[command(about = "Delete a runner group")]
Delete(RunnersGroupsDeleteCmd),
#[command(about = "Inspect effective repository access for a runner group")]
Access(RunnersGroupsAccessCmd),
}
#[derive(Args, Debug)]
pub struct RunnersGroupsListCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug, Clone)]
pub struct RunnersGroupsCreateCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Runner group name")]
pub name: String,
#[arg(long, help = "Visibility: all, selected, or private")]
pub visibility: Option<String>,
#[arg(long, help = "Optional GitHub installation ID")]
pub github_installation_id: Option<String>,
#[arg(long, help = "Optional GitHub runner group ID")]
pub github_runner_group_id: Option<i64>,
#[arg(long, help = "Optional sync state")]
pub sync_state: Option<String>,
#[arg(long, help = "Mark group as inherited")]
pub inherited: bool,
#[arg(long, help = "Allow public repositories")]
pub allows_public_repositories: bool,
#[arg(long, help = "Prompt for visibility and repository access settings")]
pub interactive: bool,
#[arg(long, help = "Restrict group to workflows")]
pub restricted_to_workflows: bool,
#[arg(long, help = "Selected workflows JSON array")]
pub selected_workflows_json: Option<String>,
#[arg(long, help = "Metadata JSON object")]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersGroupsDeleteCmd {
#[arg(help = "Runner group ID")]
pub runner_group_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersGroupsAccessCmd {
#[arg(help = "Runner group ID")]
pub runner_group_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersHostsCmd {
#[command(subcommand)]
pub command: RunnersHostsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum RunnersHostsSubCommand {
#[command(about = "List runner hosts")]
List(RunnersHostsListCmd),
#[command(about = "Enroll or upsert a runner host")]
Enroll(Box<RunnersHostsEnrollCmd>),
#[command(about = "Run host preflight checks and persist the result")]
Preflight(RunnersHostsPreflightCmd),
#[command(about = "Inspect host enrollment, heartbeat, and preflight state")]
Status(RunnersHostsStatusCmd),
}
#[derive(Args, Debug)]
pub struct RunnersHostsListCmd {
#[arg(long, help = "Optional organization ID")]
pub organization_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional status filter")]
pub status: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersHostsEnrollCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Optional daemon ID for daemon-backed hosts")]
pub daemon_id: Option<String>,
#[arg(long, help = "Host kind: daemon or rogue")]
pub host_kind: String,
#[arg(long, help = "Platform: linux, windows, or macos")]
pub platform: String,
#[arg(long, help = "Hostname")]
pub hostname: String,
#[arg(long, help = "Runner name prefix")]
pub runner_name_prefix: String,
#[arg(long, help = "Optional display name")]
pub display_name: Option<String>,
#[arg(long, help = "Optional architecture")]
pub arch: Option<String>,
#[arg(long, help = "Optional status")]
pub status: Option<String>,
#[arg(long, help = "Labels JSON object")]
pub labels_json: Option<String>,
#[arg(long, help = "Capabilities JSON object")]
pub capabilities_json: Option<String>,
#[arg(long, help = "Metadata JSON object")]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersHostsPreflightCmd {
#[arg(long, help = "Runner host ID")]
pub runner_host_id: String,
#[arg(
long,
help = "Persist the preflight result back to the control-plane API"
)]
pub write_api: bool,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersHostsStatusCmd {
#[arg(long, help = "Runner host ID")]
pub runner_host_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersInventoryCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersStatusCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Optional runner host ID")]
pub runner_host_id: Option<String>,
#[arg(long, help = "Optional runner ID")]
pub runner_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional status filter")]
pub status: Option<String>,
#[arg(long, help = "Optional phase filter")]
pub phase: Option<String>,
#[arg(long, default_value_t = 20, help = "Maximum jobs to show")]
pub limit: usize,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersLogsCmd {
#[arg(long, help = "Optional organization ID")]
pub organization_id: Option<String>,
#[arg(long, help = "Optional runner host ID")]
pub runner_host_id: Option<String>,
#[arg(long, help = "Optional runner ID")]
pub runner_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional status filter")]
pub status: Option<String>,
#[arg(long, help = "Optional phase filter")]
pub phase: Option<String>,
#[arg(long, default_value_t = 10, help = "Maximum jobs to show")]
pub limit: usize,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersSyncCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Optional runner host ID")]
pub runner_host_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional RFC3339 run-after timestamp")]
pub run_after: Option<String>,
#[arg(long, help = "Payload JSON object")]
pub payload_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersDeployCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Optional runner host ID")]
pub runner_host_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional existing runner ID")]
pub runner_id: Option<String>,
#[arg(long, help = "Optional priority")]
pub priority: Option<i32>,
#[arg(long, help = "Optional RFC3339 run-after timestamp")]
pub run_after: Option<String>,
#[arg(long, help = "Payload JSON object")]
pub payload_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersUpdateCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Optional runner host ID")]
pub runner_host_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional existing runner ID")]
pub runner_id: Option<String>,
#[arg(long, help = "Optional priority")]
pub priority: Option<i32>,
#[arg(long, help = "Optional RFC3339 run-after timestamp")]
pub run_after: Option<String>,
#[arg(long, help = "Payload JSON object")]
pub payload_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersRemoveCmd {
#[arg(long, help = "Organization ID")]
pub organization_id: String,
#[arg(long, help = "Existing runner ID")]
pub runner_id: String,
#[arg(long, help = "Optional runner host ID")]
pub runner_host_id: Option<String>,
#[arg(long, help = "Optional daemon ID")]
pub daemon_id: Option<String>,
#[arg(long, help = "Optional priority")]
pub priority: Option<i32>,
#[arg(long, help = "Optional RFC3339 run-after timestamp")]
pub run_after: Option<String>,
#[arg(long, help = "Payload JSON object")]
pub payload_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct RunnersAgentCmd {
#[command(subcommand)]
pub command: RunnersAgentSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum RunnersAgentSubCommand {
#[command(about = "Serve as a local runner host agent")]
Serve(RunnersAgentServeCmd),
}
#[derive(Args, Debug)]
pub struct RunnersAgentServeCmd {
#[arg(long, help = "Runner host ID")]
pub runner_host_id: String,
#[arg(long, default_value_t = 15, help = "Polling interval in seconds")]
pub interval_seconds: u64,
#[arg(long, help = "Process at most one job and exit")]
pub once: bool,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = "Examples:\n xbp mcp serve\n xbp mcp serve --tray\n xbp mcp serve --detach\n xbp mcp config\n xbp mcp inspector --json\n xbp mcp status\n xbp mcp install --port 1113\n xbp mcp export-tools -o crates/mcp/generated/catalog.json\n\nCursor MCP url: http://127.0.0.1:1113/sse"
)]
pub struct McpCmd {
#[command(subcommand)]
pub command: McpSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum McpSubCommand {
#[command(about = "Start the MCP HTTP/SSE server (foreground or detached)")]
Serve(McpServeCmd),
#[command(about = "Install and enable xbp-mcp.service via systemd")]
Install(McpInstallCmd),
#[command(about = "Check whether the MCP server is reachable")]
Status(McpStatusCmd),
#[command(about = "Print Cursor MCP configuration JSON")]
Config(McpConfigCmd),
#[command(about = "Print prefilled MCP Inspector URLs and tool-call presets")]
Inspector(McpInspectorCmd),
#[command(
about = "Export the full clap-derived MCP tool catalog as JSON (regenerate crates/mcp/generated/catalog.json)"
)]
ExportTools(McpExportToolsCmd),
}
#[derive(Args, Debug)]
pub struct McpExportToolsCmd {
#[arg(
long,
short = 'o',
help = "Write catalog JSON to this path (default: stdout)"
)]
pub output: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct McpServeCmd {
#[arg(long, default_value = "127.0.0.1", help = "Bind address")]
pub bind: String,
#[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
pub port: u16,
#[arg(
long,
help = "Spawn a background MCP server process and print Cursor config"
)]
pub detach: bool,
#[arg(long, help = "Use stdio MCP transport instead of HTTP/SSE")]
pub stdio: bool,
#[arg(
long,
help = "Show a system tray icon (xbp.app icon) with a Quit action"
)]
pub tray: bool,
}
#[derive(Args, Debug)]
pub struct McpInstallCmd {
#[arg(long, default_value = "127.0.0.1", help = "Bind address")]
pub bind: String,
#[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
pub port: u16,
}
#[derive(Args, Debug)]
pub struct McpStatusCmd {
#[arg(long, default_value = "127.0.0.1", help = "Bind address")]
pub bind: String,
#[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
pub port: u16,
}
#[derive(Args, Debug)]
pub struct McpConfigCmd {
#[arg(long, default_value = "127.0.0.1", help = "Bind address")]
pub bind: String,
#[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
pub port: u16,
}
#[derive(Args, Debug)]
pub struct McpInspectorCmd {
#[arg(
long,
default_value = "127.0.0.1",
help = "Bind address for the xbp MCP server"
)]
pub bind: String,
#[arg(
long,
default_value_t = 1113,
help = "HTTP port for the xbp MCP SSE transport"
)]
pub port: u16,
#[arg(long, default_value_t = 6274, help = "MCP Inspector UI port")]
pub inspector_port: u16,
#[arg(
long,
help = "Write .xbp/mcp-inspector.json for `npx @modelcontextprotocol/inspector --config`"
)]
pub write_config: bool,
#[arg(
long,
help = "Launch MCP Inspector via npx (writes config first; requires Node.js)"
)]
pub launch: bool,
#[arg(long, help = "Emit machine-readable JSON playbook")]
pub json: bool,
}
#[derive(Args, Debug)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::WORKTREE_WATCH_AFTER_HELP
)]
pub struct WorktreeWatchCmd {
#[command(subcommand)]
pub command: WorktreeWatchSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorktreeWatchSubCommand {
#[command(about = "Watch the current git worktree and append mutation events locally")]
Start(WorktreeWatchStartCmd),
#[command(about = "Stop a detached background worktree watcher")]
Stop(WorktreeWatchStopCmd),
#[command(about = "Upload unsynced local mutation spool files to xbp.app")]
Sync(WorktreeWatchSyncCmd),
#[command(
about = "Show spool path, sync backlog, and local activity stats",
after_help = crate::cli::help_render::WORKTREE_WATCH_STATUS_AFTER_HELP
)]
Status(WorktreeWatchStatusCmd),
#[command(
about = "Open a native system tray UI to start/stop watchers and view stats (Windows + Linux/GNOME)",
after_help = crate::cli::help_render::WORKTREE_WATCH_TRAY_AFTER_HELP
)]
Tray(WorktreeWatchTrayCmd),
}
#[derive(Args, Debug, Clone)]
pub struct WorktreeWatchTarget {
#[arg(
long,
conflicts_with_all = ["parent", "repos"],
help = "Git repository root, short name, or owner/repo from system inventory; defaults to the current repo"
)]
pub repo: Option<PathBuf>,
#[arg(
long,
conflicts_with_all = ["repo", "repos"],
help = "Folder containing git repositories to target recursively"
)]
pub parent: Option<PathBuf>,
#[arg(
long = "repos",
value_name = "PATH_OR_NAME",
num_args = 1..,
conflicts_with_all = ["repo", "parent"],
help = "One or more git roots, short names, or owner/repo values from system inventory (space-separated)"
)]
pub repos: Vec<PathBuf>,
}
#[derive(Args, Debug)]
pub struct WorktreeWatchTrayCmd {
#[command(flatten)]
pub target: WorktreeWatchTarget,
#[arg(
long,
help = "Keep the tray attached to the terminal instead of backgrounding"
)]
pub foreground: bool,
#[arg(
long,
default_value_t = 60,
help = "Upload interval for watchers started from the tray (seconds; 0 disables)"
)]
pub sync_interval_seconds: u64,
}
#[derive(Args, Debug)]
pub struct WorktreeWatchStartCmd {
#[command(flatten)]
pub target: WorktreeWatchTarget,
#[arg(long, help = "Spawn the watcher as a detached background process")]
pub detach: bool,
#[arg(
long,
default_value_t = 60,
help = "Upload queued events every N seconds while watching; set 0 to disable periodic sync"
)]
pub sync_interval_seconds: u64,
#[arg(long, help = "Record current git commit state once and exit")]
pub once: bool,
}
#[derive(Args, Debug)]
pub struct WorktreeWatchStopCmd {
#[command(flatten)]
pub target: WorktreeWatchTarget,
#[arg(
long,
help = "Stop the PID from watcher state even when the command line cannot be verified"
)]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct WorktreeWatchSyncCmd {
#[command(flatten)]
pub target: WorktreeWatchTarget,
#[arg(long, help = "Print the upload plan without sending it to xbp.app")]
pub dry_run: bool,
#[arg(
long,
help = "Upload all local JSONL spool files, including files already marked synced"
)]
pub resync: bool,
}
#[derive(Args, Debug)]
pub struct WorktreeWatchStatusCmd {
#[command(flatten)]
pub target: WorktreeWatchTarget,
#[arg(long, help = "Emit status as JSON")]
pub json: bool,
#[arg(
long,
help = "Print decoded mutation and commit records from local JSONL spool files"
)]
pub records: bool,
#[arg(
long,
default_value_t = 50,
requires = "records",
help = "Maximum decoded JSONL records to print per repository"
)]
pub record_limit: usize,
#[arg(
long,
help = "Generate and print activity statistics from local JSONL spool files (writes stats.json)"
)]
pub stats: bool,
#[arg(
long,
help = "Generate and print repository activity across all locally spooled branches (writes repo-activity.json)"
)]
pub repo_activity: bool,
#[arg(
long,
default_value_t = 15,
help = "Maximum minutes between mutation events counted as one coding session (used by --stats / --repo-activity)"
)]
pub stats_gap_minutes: u64,
}
#[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 runner hosts, groups, inventory, and runner jobs")]
Runners(ApiRunnersCmd),
#[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(Args, Debug)]
pub struct ApiRunnersCmd {
#[command(subcommand)]
pub command: ApiRunnersSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum ApiRunnersSubCommand {
#[command(about = "List runner hosts")]
HostsList(ApiRunnersHostsListCmd),
#[command(about = "Register or upsert a runner host")]
HostsEnroll(ApiRunnersHostsEnrollCmd),
#[command(about = "Post a heartbeat update for a runner host")]
HostsHeartbeat(ApiRunnersHostsHeartbeatCmd),
#[command(about = "List persisted preflight records for a runner host")]
HostsPreflightsList(ApiRunnersHostsPreflightsListCmd),
#[command(about = "Create or replace a persisted preflight record for a runner host")]
HostsPreflightsUpsert(ApiRunnersHostsPreflightsUpsertCmd),
#[command(about = "List runner groups for an organization")]
GroupsList(ApiRunnersGroupsListCmd),
#[command(about = "Create or upsert a runner group")]
GroupsCreate(ApiRunnersGroupsCreateCmd),
#[command(about = "Update an existing runner group")]
GroupsUpdate(ApiRunnersGroupsCreateCmd),
#[command(about = "Delete a runner group")]
GroupsDelete(ApiRunnersGroupsDeleteCmd),
#[command(about = "List repository access for a runner group")]
GroupRepositoriesList(ApiRunnersGroupRepositoriesListCmd),
#[command(about = "Grant or upsert repository access for a runner group")]
GroupRepositoriesCreate(ApiRunnersGroupRepositoriesCreateCmd),
#[command(about = "List runner inventory for an organization")]
InventoryList(ApiRunnersInventoryListCmd),
#[command(about = "Upsert a runner inventory record")]
InventoryUpsert(ApiRunnersInventoryUpsertCmd),
#[command(about = "List runner jobs")]
JobsList(ApiRunnersJobsListCmd),
#[command(about = "Create a runner job")]
JobsCreate(ApiRunnersJobsCreateCmd),
#[command(about = "Claim the next runner job")]
JobsClaim(ApiRunnersJobsClaimCmd),
#[command(about = "Update a runner job")]
JobsUpdate(ApiRunnersJobsUpdateCmd),
}
#[derive(Args, Debug)]
pub struct ApiRunnersHostsListCmd {
#[arg(long)]
pub organization_id: Option<String>,
#[arg(long)]
pub daemon_id: Option<String>,
#[arg(long)]
pub status: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersHostsEnrollCmd {
#[arg(long)]
pub organization_id: String,
#[arg(long)]
pub daemon_id: Option<String>,
#[arg(long)]
pub host_kind: String,
#[arg(long)]
pub platform: String,
#[arg(long)]
pub hostname: String,
#[arg(long)]
pub runner_name_prefix: String,
#[arg(long)]
pub display_name: Option<String>,
#[arg(long)]
pub arch: Option<String>,
#[arg(long)]
pub status: Option<String>,
#[arg(long)]
pub labels_json: Option<String>,
#[arg(long)]
pub capabilities_json: Option<String>,
#[arg(long)]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersHostsHeartbeatCmd {
#[arg(help = "Runner host ID")]
pub runner_host_id: String,
#[arg(long)]
pub status: Option<String>,
#[arg(long)]
pub labels_json: Option<String>,
#[arg(long)]
pub capabilities_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersHostsPreflightsListCmd {
#[arg(long)]
pub runner_host_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersHostsPreflightsUpsertCmd {
#[arg(long)]
pub runner_host_id: String,
#[arg(long)]
pub platform: String,
#[arg(long)]
pub status: String,
#[arg(long)]
pub docker_available: Option<bool>,
#[arg(long)]
pub service_manager: Option<String>,
#[arg(long)]
pub checks_json: Option<String>,
#[arg(long)]
pub checked_at: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug, Clone)]
pub struct ApiRunnersGroupsListCmd {
#[arg(long)]
pub organization_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug, Clone)]
pub struct ApiRunnersGroupsCreateCmd {
#[arg(long)]
pub organization_id: String,
#[arg(long)]
pub name: String,
#[arg(long)]
pub visibility: Option<String>,
#[arg(long)]
pub github_installation_id: Option<String>,
#[arg(long)]
pub github_runner_group_id: Option<i64>,
#[arg(long)]
pub sync_state: Option<String>,
#[arg(long)]
pub sync_error: Option<String>,
#[arg(long)]
pub inherited: bool,
#[arg(long)]
pub allows_public_repositories: bool,
#[arg(long)]
pub interactive: bool,
#[arg(long)]
pub restricted_to_workflows: bool,
#[arg(long)]
pub selected_workflows_json: Option<String>,
#[arg(long)]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersGroupsDeleteCmd {
#[arg(help = "Runner group ID")]
pub runner_group_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersGroupRepositoriesListCmd {
#[arg(help = "Runner group ID")]
pub runner_group_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersGroupRepositoriesCreateCmd {
#[arg(help = "Runner group ID")]
pub runner_group_id: String,
#[arg(long)]
pub github_repository_id: Option<i64>,
#[arg(long)]
pub repository_owner: String,
#[arg(long)]
pub repository_name: String,
#[arg(long)]
pub repository_full_name: String,
#[arg(long)]
pub is_private: bool,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersInventoryListCmd {
#[arg(long)]
pub organization_id: String,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersInventoryUpsertCmd {
#[arg(long)]
pub organization_id: String,
#[arg(long)]
pub runner_host_id: Option<String>,
#[arg(long)]
pub runner_group_id: Option<String>,
#[arg(long)]
pub github_runner_id: Option<i64>,
#[arg(long)]
pub name: String,
#[arg(long)]
pub platform: String,
#[arg(long)]
pub os: Option<String>,
#[arg(long)]
pub architecture: Option<String>,
#[arg(long)]
pub status: Option<String>,
#[arg(long)]
pub busy: bool,
#[arg(long)]
pub labels_json: Option<String>,
#[arg(long)]
pub metadata_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug, Clone)]
pub struct ApiRunnersJobsListCmd {
#[arg(long)]
pub organization_id: Option<String>,
#[arg(long)]
pub runner_host_id: Option<String>,
#[arg(long)]
pub runner_id: Option<String>,
#[arg(long)]
pub daemon_id: Option<String>,
#[arg(long)]
pub status: Option<String>,
#[arg(long)]
pub phase: Option<String>,
#[arg(long)]
pub limit: Option<usize>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersJobsCreateCmd {
#[arg(long)]
pub organization_id: String,
#[arg(long)]
pub runner_host_id: Option<String>,
#[arg(long)]
pub daemon_id: Option<String>,
#[arg(long)]
pub runner_id: Option<String>,
#[arg(long)]
pub job_kind: String,
#[arg(long)]
pub priority: Option<i32>,
#[arg(long)]
pub max_attempts: Option<i32>,
#[arg(long)]
pub run_after: Option<String>,
#[arg(long)]
pub payload_json: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersJobsClaimCmd {
#[arg(long)]
pub runner_host_id: Option<String>,
#[arg(long)]
pub daemon_id: Option<String>,
#[arg(long)]
pub locked_by: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[derive(Args, Debug)]
pub struct ApiRunnersJobsUpdateCmd {
#[arg(help = "Runner job ID")]
pub runner_job_id: String,
#[arg(long)]
pub status: String,
#[arg(long)]
pub error_text: Option<String>,
#[command(flatten)]
pub target: ApiTargetOptions,
}
#[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)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::GENERATE_AFTER_HELP
)]
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),
#[cfg(feature = "openapi-gen")]
#[command(about = "Generate static OpenAPI documents for configured services")]
Openapi(GenerateOpenApiCmd),
Systemd(GenerateSystemdCmd),
}
#[cfg(feature = "openapi-gen")]
#[derive(Args, Debug)]
pub struct GenerateOpenApiCmd {
#[arg(long, action = clap::ArgAction::Append, help = "Generate only the named service (repeatable)")]
pub service: Vec<String>,
#[arg(
long,
conflicts_with = "service",
help = "Generate every eligible service and the aggregate"
)]
pub all: bool,
#[arg(long, value_parser = ["yaml", "json", "both"], help = "Override configured output formats")]
pub format: Option<String>,
#[arg(
long,
help = "Override the output path (requires one service and one format)"
)]
pub output: Option<PathBuf>,
#[arg(long, help = "Fail if generated output differs without writing files")]
pub check: bool,
#[arg(long, help = "Render and validate without writing files")]
pub dry_run: bool,
#[arg(long, help = "Fail when a source type cannot be resolved")]
pub strict: bool,
#[arg(long, help = "Ignore the parsed-source cache")]
pub no_cache: bool,
}
#[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>,
}
#[derive(Args, Debug)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = "Examples:\n xbp workers list\n xbp workers list --failed -n 10\n xbp workers ls\n xbp workers logs -f\n xbp workers logs --build --failed -n 200 -o build.log\n xbp workers logs --build --grep error --errors-only\n xbp workers secrets put --environment production --name GITHUB_APP_PRIVATE_KEY --from-stdin\n xbp workers secrets list --environment production\n xbp workers settings get --environment production\n xbp workers wrangler generate-config --output wrangler.deploy.json\n xbp workers d1 migrations apply DB --remote\n xbp workers deploy configure --write-config\n xbp workers deploy run --app athena-studio\n xbp workers deploy ci --app athena --all\n xbp workers deploy sync-env-local\n xbp workers deploy ci --version-upload\n xbp workers worktree link-dev-vars"
)]
pub struct WorkersCmd {
#[arg(
long,
help = "Workers project root (defaults to current dir, or apps/web inside the current XBP repo when present)"
)]
pub root: Option<PathBuf>,
#[arg(
long,
help = "Worker app name from .xbp/xbp.yaml `workers:` (defaults to the app matching the current directory)"
)]
pub app: Option<String>,
#[arg(long, help = "Cloudflare API token override")]
pub token: Option<String>,
#[arg(long, help = "Cloudflare account ID override")]
pub account_id: Option<String>,
#[command(subcommand)]
pub command: WorkersSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersSubCommand {
#[command(
alias = "ls",
about = "List Cloudflare Worker scripts with build and placement status",
after_help = "Examples:\n xbp workers list\n xbp workers list --all\n xbp workers list --failed -n 10\n xbp workers list --status running --sort modified\n xbp workers list --json"
)]
List(WorkersListCmd),
#[command(
about = "Show deployment status, stream runtime logs, or fetch Workers Builds CI logs",
after_help = "Examples:\n xbp workers logs --worker suits-formations --json\n xbp workers logs -f\n xbp workers logs -n 100 -o runtime.log\n xbp workers logs xbp-production\n xbp workers logs --build --wait\n xbp workers logs --build --failed -n 200\n xbp workers logs --build --grep error --errors-only\n xbp workers logs --build --list-builds"
)]
Logs(WorkersLogsCmd),
#[command(about = "Manage secret bindings for a Worker script or Wrangler environment")]
Secrets(WorkersSecretsCmd),
#[command(about = "Fetch remote Worker settings via the Cloudflare API")]
Settings(WorkersSettingsCmd),
#[command(about = "Inspect and synchronize Durable Object namespace configuration")]
DurableObjects(WorkersDurableObjectsCmd),
#[command(about = "Inspect or generate Wrangler config helpers")]
Wrangler(WorkersWranglerCmd),
#[command(about = "Run Wrangler D1 migration helpers")]
D1(WorkersD1Cmd),
#[command(about = "Run Worker deploy and predeploy helpers")]
Deploy(WorkersDeployCmd),
#[command(about = "Inspect shared worktree paths or link shared dev files")]
Worktree(WorkersWorktreeCmd),
#[command(about = "Show resolved Worker runtime metadata from local env and config files")]
Env(WorkersEnvCmd),
}
#[derive(Args, Debug, Default)]
pub struct WorkersListCmd {
#[arg(
long,
help = "Show every Worker in the account instead of only Workers for this XBP project"
)]
pub all: bool,
#[arg(long, help = "Output the worker inventory as JSON")]
pub json: bool,
#[arg(short = 'n', long = "limit", help = "Show at most N workers")]
pub limit: Option<usize>,
#[arg(
long = "status",
help = "Filter by build status (failed, success, running, unknown)"
)]
pub status: Option<String>,
#[arg(long, help = "Shortcut for --status failed")]
pub failed: bool,
#[arg(
long = "sort",
default_value = "name",
help = "Sort workers by name, modified, or status"
)]
pub sort: String,
#[arg(long = "no-color", help = "Disable ANSI colors in table output")]
pub no_color: bool,
}
#[derive(Args, Debug)]
pub struct WorkersLogsCmd {
#[command(flatten)]
pub target: WorkersTargetArgs,
#[arg(
short = 'f',
long = "follow",
help = "Stream runtime logs until interrupted (wrangler tail)"
)]
pub follow: bool,
#[arg(
long = "build",
help = "Show Workers Builds CI logs instead of runtime deployment output"
)]
pub build: bool,
#[arg(
long = "failed",
help = "When using --build, prefer the latest failed build"
)]
pub failed: bool,
#[arg(long, help = "Output logs as JSON")]
pub json: bool,
#[arg(
short = 'n',
long = "lines",
help = "Show only the last N log lines (deployments/build output)"
)]
pub lines: Option<usize>,
#[arg(
short = 'o',
long = "output",
help = "Write log output to a file instead of stdout"
)]
pub output: Option<PathBuf>,
#[arg(
short = 'g',
long = "grep",
help = "Filter log lines matching this pattern (case-insensitive substring)"
)]
pub grep: Option<String>,
#[arg(
short = 'e',
long = "errors-only",
help = "Show only lines that look like errors or failures"
)]
pub errors_only: bool,
#[arg(long = "no-color", help = "Disable ANSI colors in log output")]
pub no_color: bool,
#[arg(
long = "list-builds",
help = "List recent Workers Builds before fetching logs (with --build)"
)]
pub list_builds: bool,
#[arg(
long = "build-index",
help = "Select build by index from --list-builds (0 = latest)"
)]
pub build_index: Option<usize>,
#[arg(
long,
help = "Poll until the selected Workers Build finishes before fetching logs"
)]
pub wait: bool,
#[arg(long = "no-wait", help = "Skip polling for in-progress Workers Builds")]
pub no_wait: bool,
#[arg(
long = "wait-seconds",
default_value_t = 120,
help = "Maximum seconds to poll an in-progress Workers Build"
)]
pub wait_seconds: u64,
#[arg(help = "Worker script name. When omitted, xbp prompts interactively.")]
pub script_name: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct WorkersTargetArgs {
#[arg(
long,
help = "Worker base name (defaults to wrangler config name or xbp)"
)]
pub worker: Option<String>,
#[arg(
long = "environment",
alias = "env",
help = "Wrangler environment name. The remote script resolves to <worker>-<environment>."
)]
pub environment: Option<String>,
#[arg(
long,
help = "Exact remote script name override. When set, this bypasses <worker>-<environment> resolution."
)]
pub script: Option<String>,
}
#[derive(Args, Debug)]
pub struct WorkersSecretsCmd {
#[command(flatten)]
pub target: WorkersTargetArgs,
#[command(subcommand)]
pub command: WorkersSecretsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersSecretsSubCommand {
#[command(about = "List secret bindings on the resolved Worker script")]
List(WorkersSecretsListCmd),
#[command(about = "Fetch one secret binding metadata or value")]
Get(WorkersSecretsGetCmd),
#[command(about = "Create or update a secret binding")]
Put(WorkersSecretsPutCmd),
#[command(about = "Delete a secret binding")]
Delete(WorkersSecretsDeleteCmd),
#[command(about = "Create, update, or delete multiple secret bindings from a file")]
Bulk(WorkersSecretsBulkCmd),
}
#[derive(Args, Debug, Default)]
pub struct WorkersSecretsListCmd {}
#[derive(Args, Debug)]
pub struct WorkersSecretsGetCmd {
#[arg(long, help = "Secret binding name")]
pub name: String,
}
#[derive(Args, Debug)]
pub struct WorkersSecretsPutCmd {
#[arg(long, help = "Secret binding name")]
pub name: String,
#[arg(long, help = "Secret value")]
pub value: Option<String>,
#[arg(long, help = "Read the secret value from stdin instead of --value")]
pub from_stdin: bool,
}
#[derive(Args, Debug)]
pub struct WorkersSecretsDeleteCmd {
#[arg(long, help = "Secret binding name")]
pub name: String,
}
#[derive(Args, Debug)]
pub struct WorkersSecretsBulkCmd {
#[arg(long, help = "Path to a .env or JSON file containing secret updates")]
pub file: PathBuf,
#[arg(
long,
default_value = "env",
help = "Input format: env or json. For json, pass an object mapping names to string values or null for deletes."
)]
pub format: String,
}
#[derive(Args, Debug)]
pub struct WorkersSettingsCmd {
#[command(flatten)]
pub target: WorkersTargetArgs,
}
#[derive(Args, Debug)]
pub struct WorkersDurableObjectsCmd {
#[command(flatten)]
pub target: WorkersTargetArgs,
#[command(subcommand)]
pub command: WorkersDurableObjectsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersDurableObjectsSubCommand {
#[command(about = "List configured and live Durable Object namespaces")]
List(WorkersDurableObjectsListCmd),
#[command(about = "Compare local Durable Object configuration with live namespaces")]
Inspect(WorkersDurableObjectsInspectCmd),
#[command(about = "Import live namespace metadata into XBP configuration")]
Sync(WorkersDurableObjectsSyncCmd),
}
#[derive(Args, Debug, Default)]
pub struct WorkersDurableObjectsListCmd {
#[arg(long, help = "Include every namespace in the account")]
pub all: bool,
#[arg(long, help = "Output JSON")]
pub json: bool,
}
#[derive(Args, Debug, Default)]
pub struct WorkersDurableObjectsInspectCmd {
#[arg(long, help = "Include every namespace in the account")]
pub all: bool,
#[arg(long, help = "Output JSON")]
pub json: bool,
}
#[derive(Args, Debug, Default)]
pub struct WorkersDurableObjectsSyncCmd {
#[arg(
long,
help = "Write imported namespace metadata to the local XBP config"
)]
pub apply: bool,
#[arg(
long,
help = "Confirm generation of deploy-affecting migration changes"
)]
pub confirm_migrations: bool,
}
#[derive(Args, Debug)]
pub struct WorkersWranglerCmd {
#[command(subcommand)]
pub command: WorkersWranglerSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersWranglerSubCommand {
#[command(about = "Generate a Wrangler deploy config JSON file from env vars")]
GenerateConfig(WorkersWranglerGenerateConfigCmd),
#[command(about = "Resolve which Wrangler config file local dev should use")]
ConfigPath(WorkersWranglerConfigPathCmd),
#[command(
name = "run",
alias = "exec",
about = "Run any official Wrangler command from the selected Worker root",
after_help = "Pass Wrangler arguments after `--`. Examples:\n xbp workers wrangler run -- d1 list\n xbp workers wrangler run -- d1 execute DB --remote --file schema.sql\n xbp workers wrangler run -- r2 bucket create r2-bucket\n xbp workers wrangler run -- r2 bucket cors set r2-bucket --file cors.json\n xbp workers wrangler run -- queues create xbp-events\n xbp workers wrangler run -- secrets-store store list --remote"
)]
Run(WorkersWranglerRunCmd),
}
#[derive(Args, Debug)]
pub struct WorkersWranglerGenerateConfigCmd {
#[arg(
long,
default_value = "wrangler.deploy.json",
help = "Output filename, relative to the worker root unless absolute"
)]
pub output: PathBuf,
}
#[derive(Args, Debug)]
pub struct WorkersWranglerConfigPathCmd {
#[arg(
long,
default_value = "serve",
help = "Calling command name, for example serve"
)]
pub command_name: String,
#[arg(
long,
default_value = "development",
help = "Execution mode, for example development or production"
)]
pub mode: String,
}
#[derive(Args, Debug)]
pub struct WorkersWranglerRunCmd {
#[arg(
required = true,
trailing_var_arg = true,
allow_hyphen_values = true,
help = "Arguments passed verbatim to the official Wrangler CLI"
)]
pub args: Vec<String>,
}
#[derive(Args, Debug)]
pub struct WorkersD1Cmd {
#[command(subcommand)]
pub command: WorkersD1SubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersD1SubCommand {
#[command(about = "Apply pending Wrangler D1 migrations")]
Migrations(WorkersD1MigrationsCmd),
}
#[derive(Args, Debug)]
pub struct WorkersD1MigrationsCmd {
#[command(subcommand)]
pub command: WorkersD1MigrationsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersD1MigrationsSubCommand {
#[command(about = "Apply pending migrations to a local or remote D1 database")]
Apply(WorkersD1MigrationsApplyCmd),
}
#[derive(Args, Debug)]
pub struct WorkersD1MigrationsApplyCmd {
#[arg(help = "D1 database binding or name, for example DB")]
pub database: String,
#[arg(
long,
conflicts_with = "remote",
help = "Apply migrations to the local Wrangler D1 database"
)]
pub local: bool,
#[arg(
long,
conflicts_with = "local",
help = "Apply migrations to the remote D1 database"
)]
pub remote: bool,
#[arg(long, help = "Wrangler config path override")]
pub config: Option<PathBuf>,
#[command(flatten)]
pub target: WorkersTargetArgs,
#[arg(
long,
help = "Persist local D1 state to this directory. When omitted in a git worktree, xbp uses the shared .wrangler/state path automatically."
)]
pub persist_to: Option<PathBuf>,
#[arg(
long,
help = "Disable the automatic shared .wrangler/state path when running local migrations from a git worktree"
)]
pub no_shared_worktree_state: bool,
}
#[derive(Args, Debug)]
pub struct WorkersDeployCmd {
#[arg(
long,
help = "Run the deploy action for every worker app configured in .xbp/xbp.yaml"
)]
pub all: bool,
#[command(subcommand)]
pub command: WorkersDeploySubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersDeploySubCommand {
#[command(
about = "Discover worker apps, sync Wrangler config, and optionally write .xbp/xbp.yaml"
)]
Configure(WorkersDeployConfigureCmd),
#[command(about = "Run the configured deploy command for one or more worker apps")]
Run(WorkersDeployRunCmd),
#[command(about = "Run the predeploy sync flow unless Workers CI mode is active")]
Predeploy(WorkersDeployPredeployCmd),
#[command(about = "Read .env.local or process env, then emit .dev.vars and Wrangler configs")]
SyncEnvLocal(WorkersDeploySyncEnvLocalCmd),
#[command(
about = "Run the built-in Cloudflare Worker CI deploy workflow (build + wrangler deploy)"
)]
Ci(WorkersDeployCiCmd),
#[command(
about = "Run the existing deploy-selection flow that chooses CI or local deploy behavior"
)]
Select(WorkersDeploySelectCmd),
}
#[derive(Args, Debug, Default)]
pub struct WorkersDeployConfigureCmd {
#[arg(
long,
help = "Scan the repo for Wrangler projects and merge them into .xbp/xbp.yaml `workers:`"
)]
pub write_config: bool,
#[arg(
long,
help = "Preview worker discovery and config writes without changing files"
)]
pub dry_run: bool,
}
#[derive(Args, Debug, Default)]
pub struct WorkersDeployRunCmd {}
#[derive(Args, Debug)]
pub struct WorkersDeployPredeployCmd {
#[arg(long, help = "Force Workers CI mode and skip local sync")]
pub ci: bool,
}
#[derive(Args, Debug)]
pub struct WorkersDeploySyncEnvLocalCmd {}
#[derive(Args, Debug)]
pub struct WorkersDeployCiCmd {
#[arg(long, help = "Upload a new version without immediately deploying it")]
pub version_upload: bool,
}
#[derive(Args, Debug)]
pub struct WorkersDeploySelectCmd {
#[arg(long, help = "Force the WORKERS_CI=1 branch of the selector")]
pub ci: bool,
#[arg(
long,
help = "Branch name to expose as WORKERS_CI_BRANCH when --ci is set"
)]
pub branch: Option<String>,
}
#[derive(Args, Debug)]
pub struct WorkersWorktreeCmd {
#[command(subcommand)]
pub command: WorkersWorktreeSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum WorkersWorktreeSubCommand {
#[command(about = "Print repo-root, primary worktree, and shared Wrangler state paths")]
Paths(WorkersWorktreePathsCmd),
#[command(
about = "Symlink apps/web/.dev.vars and wrangler.dev.jsonc from the primary worktree when in a linked worktree"
)]
LinkDevVars(WorkersWorktreeLinkDevVarsCmd),
}
#[derive(Args, Debug, Default)]
pub struct WorkersWorktreePathsCmd {}
#[derive(Args, Debug, Default)]
pub struct WorkersWorktreeLinkDevVarsCmd {}
#[derive(Args, Debug)]
pub struct WorkersEnvCmd {
#[command(flatten)]
pub target: WorkersTargetArgs,
#[arg(
long,
help = "Show resolved plain-text binding values instead of masking them"
)]
pub show_values: bool,
}
#[derive(Args, Debug)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = "Examples:\n xbp cloudflare doctor --app auth\n xbp cloudflare init --app auth --worker-root apps/auth-worker --container-port 8787 --health-path /health --write\n xbp cloudflare deploy --app auth --rollout immediate --dry-run\n xbp cloudflare release --app auth --version 1.2.3 --rollout gradual\n xbp cloudflare containers instances --app auth\n xbp cloudflare jobs enqueue deploy --app auth --rollout immediate"
)]
pub struct CloudflareCmd {
#[arg(
long,
global = true,
help = "XBP project root (defaults to the nearest directory containing .xbp/xbp.yaml)"
)]
pub root: Option<PathBuf>,
#[arg(
long,
global = true,
help = "Worker app name from .xbp/xbp.yaml `workers:`"
)]
pub app: Option<String>,
#[arg(long, global = true, help = "Cloudflare API token override")]
pub token: Option<String>,
#[arg(long, global = true, help = "Cloudflare account ID override")]
pub account_id: Option<String>,
#[command(subcommand)]
pub command: CloudflareSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CloudflareSubCommand {
#[command(
about = "Validate Wrangler, container metadata, bindings, secrets, and deploy readiness"
)]
Doctor(CloudflareDoctorCmd),
#[command(about = "Discover or write the project-level Worker container contract")]
Init(CloudflareInitCmd),
#[command(about = "Run the canonical Worker + Container deploy workflow")]
Deploy(CloudflareDeployCmd),
#[command(about = "Sync a version-bearing deploy var, deploy, and verify live health")]
Release(CloudflareReleaseCmd),
#[command(about = "Wrap official Wrangler container commands from the selected app root")]
Containers(CloudflareContainersCmd),
#[command(
about = "Manage Cloudflare Tunnels through official Wrangler commands",
after_help = "Tunnel commands are experimental in Wrangler. Examples:\n xbp cloudflare tunnel create my-app\n xbp cloudflare tunnel list\n xbp cloudflare tunnel info my-app\n xbp cloudflare tunnel run my-app\n xbp cloudflare tunnel run --token <TOKEN>\n xbp cloudflare tunnel quick-start http://localhost:8080\n xbp cloudflare tunnel delete my-app --force"
)]
Tunnel(CloudflareTunnelCmd),
#[command(
about = "Manage Cloudflare Workflows through official Wrangler commands",
after_help = "Pass the documented Wrangler workflows command after `workflows`. Examples:\n xbp cloudflare workflows list\n xbp cloudflare workflows describe my-workflow\n xbp cloudflare workflows trigger my-workflow '{\"key\":\"value\"}'\n xbp cloudflare workflows instances list my-workflow --local\n xbp cloudflare workflows instances describe my-workflow latest\n xbp cloudflare workflows instances send-event my-workflow latest --type my-event --payload '{\"key\":\"value\"}'\n xbp cloudflare workflows instances pause my-workflow latest\n xbp cloudflare workflows instances resume my-workflow latest\n xbp cloudflare workflows instances restart my-workflow latest\n xbp cloudflare workflows instances terminate my-workflow latest"
)]
Workflows(CloudflareWorkflowsCmd),
#[command(
about = "Run Wrangler local-development commands with environment-specific vars",
after_help = "Pass the documented Wrangler local-development command after `env`. Examples:\n xbp cloudflare env dev\n xbp cloudflare env dev --env staging\n xbp cloudflare env dev --env staging --port 8787"
)]
Env(CloudflareEnvCmd),
#[command(about = "Manage local file-backed Cloudflare workflow jobs")]
Jobs(CloudflareJobsCmd),
}
#[derive(Args, Debug, Default)]
pub struct CloudflareDoctorCmd {
#[arg(
long,
help = "Skip live Wrangler calls and only validate local files/config"
)]
pub offline: bool,
}
#[derive(Args, Debug)]
pub struct CloudflareInitCmd {
#[arg(long, help = "Worker app root to store in .xbp/xbp.yaml")]
pub worker_root: PathBuf,
#[arg(long, help = "Container port exposed by the runtime")]
pub container_port: u16,
#[arg(
long,
default_value = "/health",
help = "Container health endpoint path"
)]
pub health_path: String,
#[arg(long, help = "Container Durable Object class name")]
pub class_name: Option<String>,
#[arg(long, help = "Worker binding name for the container Durable Object")]
pub binding: Option<String>,
#[arg(
long,
default_value = "Dockerfile",
help = "Dockerfile path relative to the Worker root"
)]
pub dockerfile: PathBuf,
#[arg(long, help = "Cloudflare container application id")]
pub application_id: Option<String>,
#[arg(
long = "required-secret",
help = "Required secret/env binding name",
value_name = "NAME"
)]
pub required_secrets: Vec<String>,
#[arg(long, help = "Wrangler vars key that should carry release versions")]
pub version_var: Option<String>,
#[arg(
long,
help = "Persist the discovered/scaffolded contract to .xbp/xbp.yaml"
)]
pub write: bool,
}
#[derive(Args, Debug)]
pub struct CloudflareDeployCmd {
#[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
pub rollout: CloudflareRollout,
#[arg(long, help = "Run validation and Wrangler deploy --dry-run only")]
pub dry_run: bool,
#[arg(
long,
help = "Run post-deploy verification without invoking Wrangler deploy"
)]
pub skip_deploy: bool,
#[arg(
long,
help = "Allow the container image tag to remain unchanged after deploy verification"
)]
pub allow_unchanged_container_image: bool,
#[arg(
long,
help = "Delete old container image tags after successful verification"
)]
pub prune_old_images: bool,
#[arg(long, help = "Image tag count to retain when pruning old tags")]
pub keep_image_tag_count: Option<usize>,
}
#[derive(Args, Debug)]
pub struct CloudflareReleaseCmd {
#[arg(long, help = "Release version to write to the configured version var")]
pub version: String,
#[arg(
long,
help = "Release domain that must validate before this container-backed Worker release"
)]
pub domain: Option<String>,
#[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
pub rollout: CloudflareRollout,
#[arg(
long,
help = "Run release verification without invoking Wrangler deploy"
)]
pub skip_deploy: bool,
#[arg(
long,
help = "Allow the container image tag to remain unchanged after release verification"
)]
pub allow_unchanged_container_image: bool,
#[arg(
long,
help = "Delete old container image tags after successful verification"
)]
pub prune_old_images: bool,
#[arg(long, help = "Image tag count to retain when pruning old tags")]
pub keep_image_tag_count: Option<usize>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum CloudflareRollout {
Immediate,
Gradual,
None,
}
impl CloudflareRollout {
pub fn as_str(self) -> &'static str {
match self {
Self::Immediate => "immediate",
Self::Gradual => "gradual",
Self::None => "none",
}
}
}
#[derive(Args, Debug)]
pub struct CloudflareContainersCmd {
#[command(subcommand)]
pub command: CloudflareContainersSubCommand,
}
#[derive(Args, Debug)]
pub struct CloudflareTunnelCmd {
#[command(subcommand)]
pub command: CloudflareTunnelSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CloudflareTunnelSubCommand {
#[command(about = "Create a remotely managed Cloudflare Tunnel")]
Create(CloudflareTunnelCreateCmd),
#[command(about = "Delete a Cloudflare Tunnel")]
Delete(CloudflareTunnelDeleteCmd),
#[command(about = "Show details for a Cloudflare Tunnel")]
Info(CloudflareTunnelInfoCmd),
#[command(about = "List Cloudflare Tunnels")]
List(CloudflareTunnelListCmd),
#[command(about = "Run a named or token-authenticated Cloudflare Tunnel")]
Run(CloudflareTunnelRunCmd),
#[command(about = "Start a temporary anonymous Quick Tunnel")]
QuickStart(CloudflareTunnelQuickStartCmd),
}
#[derive(Args, Debug)]
pub struct CloudflareTunnelCreateCmd {
#[arg(help = "Unique tunnel name within the Cloudflare account")]
pub name: String,
}
#[derive(Args, Debug)]
pub struct CloudflareTunnelDeleteCmd {
#[arg(help = "Tunnel name or UUID")]
pub tunnel: String,
#[arg(long, help = "Skip the confirmation prompt")]
pub force: bool,
}
#[derive(Args, Debug)]
pub struct CloudflareTunnelInfoCmd {
#[arg(help = "Tunnel name or UUID")]
pub tunnel: String,
}
#[derive(Args, Debug, Default)]
pub struct CloudflareTunnelListCmd {}
#[derive(Args, Debug)]
pub struct CloudflareTunnelRunCmd {
#[arg(help = "Tunnel name or UUID; omit when using --token")]
pub tunnel: Option<String>,
#[arg(long, help = "Tunnel token; avoids API lookup and authentication")]
pub token: Option<String>,
#[arg(
long,
help = "cloudflared log level: debug, info, warn, error, or fatal"
)]
pub log_level: Option<String>,
}
#[derive(Args, Debug)]
pub struct CloudflareTunnelQuickStartCmd {
#[arg(help = "Local URL to expose, for example http://localhost:8080")]
pub url: String,
}
#[derive(Args, Debug)]
pub struct CloudflareWorkflowsCmd {
#[command(subcommand)]
pub command: CloudflareWorkflowsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CloudflareWorkflowsSubCommand {
#[command(external_subcommand)]
External(Vec<String>),
}
#[derive(Args, Debug)]
pub struct CloudflareEnvCmd {
#[command(subcommand)]
pub command: CloudflareEnvSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CloudflareEnvSubCommand {
#[command(external_subcommand)]
External(Vec<String>),
}
#[derive(Subcommand, Debug)]
pub enum CloudflareContainersSubCommand {
#[command(about = "Run wrangler containers list")]
List(CloudflareContainersListCmd),
#[command(
about = "Run wrangler containers info for the configured or supplied application id"
)]
Info(CloudflareContainersInfoCmd),
#[command(
about = "Run wrangler containers instances for the configured or supplied application id"
)]
Instances(CloudflareContainersInstancesCmd),
#[command(about = "Run wrangler containers push")]
Push(CloudflareContainersPushCmd),
#[command(about = "Open wrangler containers ssh for an instance")]
Ssh(CloudflareContainersSshCmd),
}
#[derive(Args, Debug, Default)]
pub struct CloudflareContainersListCmd {
#[arg(long, help = "Ask Wrangler for JSON output when supported")]
pub json: bool,
}
#[derive(Args, Debug, Default)]
pub struct CloudflareContainersInfoCmd {
#[arg(long, help = "Container application id override")]
pub application_id: Option<String>,
#[arg(long, help = "Ask Wrangler for JSON output when supported")]
pub json: bool,
}
#[derive(Args, Debug, Default)]
pub struct CloudflareContainersInstancesCmd {
#[arg(long, help = "Container application id override")]
pub application_id: Option<String>,
#[arg(long, help = "Ask Wrangler for JSON output when supported")]
pub json: bool,
}
#[derive(Args, Debug)]
pub struct CloudflareContainersPushCmd {
#[arg(help = "Image tag or image name to push through Wrangler")]
pub image: String,
}
#[derive(Args, Debug)]
pub struct CloudflareContainersSshCmd {
#[arg(help = "Container instance id")]
pub instance_id: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
pub args: Vec<String>,
}
#[derive(Args, Debug)]
pub struct CloudflareJobsCmd {
#[command(subcommand)]
pub command: CloudflareJobsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CloudflareJobsSubCommand {
#[command(about = "Queue a local Cloudflare workflow job")]
Enqueue(CloudflareJobsEnqueueCmd),
#[command(about = "Run queued local Cloudflare workflow jobs")]
Run(CloudflareJobsRunCmd),
#[command(about = "Show one local Cloudflare workflow job")]
Status(CloudflareJobsStatusCmd),
#[command(about = "List local Cloudflare workflow jobs")]
List(CloudflareJobsListCmd),
#[command(about = "Print stored logs for one local Cloudflare workflow job")]
Logs(CloudflareJobsLogsCmd),
}
#[derive(Args, Debug)]
pub struct CloudflareJobsEnqueueCmd {
#[arg(value_enum)]
pub workflow: CloudflareJobWorkflow,
#[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
pub rollout: CloudflareRollout,
#[arg(long, help = "Required for release jobs")]
pub version: Option<String>,
#[arg(long, help = "Queue deploy jobs in dry-run mode")]
pub dry_run: bool,
#[arg(long, help = "Queue verification without invoking Wrangler deploy")]
pub skip_deploy: bool,
#[arg(long, help = "Allow an unchanged container image during verification")]
pub allow_unchanged_container_image: bool,
#[arg(long, help = "Prune old container image tags after verification")]
pub prune_old_images: bool,
#[arg(long, help = "Image tag count to retain when pruning old tags")]
pub keep_image_tag_count: Option<usize>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
#[serde(rename_all = "kebab-case")]
pub enum CloudflareJobWorkflow {
Doctor,
Deploy,
Release,
}
impl CloudflareJobWorkflow {
pub fn as_str(self) -> &'static str {
match self {
Self::Doctor => "doctor",
Self::Deploy => "deploy",
Self::Release => "release",
}
}
}
#[derive(Args, Debug, Default)]
pub struct CloudflareJobsRunCmd {
#[arg(long, help = "Run only the next queued job")]
pub once: bool,
}
#[derive(Args, Debug)]
pub struct CloudflareJobsStatusCmd {
pub job_id: String,
}
#[derive(Args, Debug, Default)]
pub struct CloudflareJobsListCmd {}
#[derive(Args, Debug)]
pub struct CloudflareJobsLogsCmd {
pub job_id: String,
}
#[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",
default_value = "xbp-dev",
help = "Environment to sync (default: xbp-dev). Nested services are scoped automatically, e.g. xbp-dev-web."
)]
pub environment: String,
#[arg(
long,
help = "Service name from .xbp/xbp.yaml. If omitted, XBP resolves it from the current directory or prompts when ambiguous."
)]
pub service: Option<String>,
#[command(subcommand)]
pub command: Option<SecretsSubCommand>,
}
#[cfg(feature = "secrets")]
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum SecretsProviderKind {
Github,
Cloudflare,
Railway,
Vercel,
}
#[cfg(feature = "secrets")]
#[derive(Subcommand, Debug)]
pub enum SecretsSubCommand {
/// List available secrets providers
#[command(alias = "ls", alias = "list-providers")]
Providers,
/// List local env vars from the preferred env file
List(ListCmd),
/// Push local env vars to the secrets provider (GitHub)
Push(PushCmd),
/// Pull secrets from the provider into .env.local
Pull(PullCmd),
/// Generate .env.default from source code inspection
GenerateDefault(GenerateDefaultCmd),
/// Generate .env.example with categories and defaults
GenerateExample(GenerateExampleCmd),
/// Compare local env with remote (GitHub) variables
Diff,
/// Verify that all required env vars are available locally
Verify,
/// Check connectivity, token scope, and repo access for secrets
#[command(name = "diag", alias = "doctor")]
Diag,
/// Manage Cloudflare secrets stores
Stores(SecretsStoresCmd),
/// Manage Cloudflare secrets in a store
Secrets(CloudflareSecretsCmd),
/// Inspect Cloudflare quota usage
Quota(SecretsQuotaCmd),
/// Show secrets command usage
#[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 = "dev-vars-file",
help = "Path to Cloudflare .dev.vars file. Defaults to .dev.vars when it exists."
)]
pub dev_vars_file: Option<String>,
#[arg(
long,
help = "Skip the Cloudflare .dev.vars GitHub environment sync lane"
)]
pub skip_dev_vars: bool,
#[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>,
#[arg(
long = "dev-vars-output",
help = "Output path for Cloudflare .dev.vars. Defaults to .dev.vars when it already exists."
)]
pub dev_vars_output: Option<String>,
#[arg(
long,
help = "Pull the Cloudflare .dev.vars environment even when .dev.vars does not exist yet"
)]
pub include_dev_vars: bool,
#[arg(
long,
help = "Skip the Cloudflare .dev.vars GitHub environment sync lane"
)]
pub skip_dev_vars: bool,
}
#[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 {}
const DNS_HELP_TEMPLATE: &str = crate::cli::help_render::XBP_HELP_TEMPLATE;
const DNS_COMMAND_AFTER_HELP: &str = "\
Examples:
xbp dns providers
xbp dns zones list --provider cloudflare --account-id acc_123
xbp dns records list --provider cloudflare --zone-id zone_123
xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
xbp dns dnssec get --provider cloudflare --zone-id zone_123
xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
Notes:
Start with `xbp dns providers` to see what is implemented today.
Cloudflare auth comes from `--token`, `CLOUDFLARE_API_TOKEN`, `xbp config cloudflare set-key`, or a linked dashboard account after `xbp login` (`xbp config cloudflare login`).";
const DNS_ZONES_AFTER_HELP: &str = "\
Examples:
xbp dns zones list --provider cloudflare --account-id acc_123
xbp dns zones get --provider cloudflare --zone-id zone_123
xbp dns zones create --provider cloudflare --name example.com --account-id acc_123 --jump-start
xbp dns zones edit --provider cloudflare --zone-id zone_123 --paused true
xbp dns zones delete --provider cloudflare --zone-id zone_123";
const DNS_RECORDS_AFTER_HELP: &str = "\
Examples:
xbp dns records list --provider cloudflare --zone-id zone_123
xbp dns records get --provider cloudflare --zone-id zone_123 --record-id rec_123
xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
xbp dns records edit --provider cloudflare --zone-id zone_123 --record-id rec_123 --proxied true
xbp dns records import --provider cloudflare --zone-id zone_123 --file zone.txt
xbp dns records export --provider cloudflare --zone-id zone_123 --output zone.txt";
const DNS_DNSSEC_AFTER_HELP: &str = "\
Examples:
xbp dns dnssec get --provider cloudflare --zone-id zone_123
xbp dns dnssec edit --provider cloudflare --zone-id zone_123 --status active";
const DNS_SETTINGS_AFTER_HELP: &str = "\
Examples:
xbp dns settings get --provider cloudflare --zone-id zone_123
xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
xbp dns settings edit --provider cloudflare --zone-id zone_123 --nameservers-type custom --nameservers-ns-set 2";
const DNS_PROVIDERS_AFTER_HELP: &str = "\
Examples:
xbp dns providers
What this shows:
Implemented providers are wired into `xbp dns` today.
Planned providers are tracked in the CLI surface but not callable yet.";
#[derive(Args, Debug)]
#[command(
about = "Manage DNS providers, zones, records, DNSSEC, and provider-level settings",
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = DNS_HELP_TEMPLATE,
after_help = DNS_COMMAND_AFTER_HELP
)]
pub struct DnsCmd {
#[command(subcommand)]
pub command: DnsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsSubCommand {
#[command(
alias = "ls",
alias = "list",
about = "List supported DNS providers and current implementation status",
after_help = DNS_PROVIDERS_AFTER_HELP
)]
Providers,
#[command(about = "Inspect and manage DNS zones")]
Zones(DnsZonesCmd),
#[command(about = "List, create, edit, import, export, and batch DNS records")]
Records(DnsRecordsCmd),
#[command(about = "Inspect or edit DNSSEC status for a zone")]
Dnssec(DnssecCmd),
#[command(about = "Inspect or edit provider DNS settings for a zone")]
Settings(DnsSettingsCmd),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
pub enum DnsProviderKind {
Cloudflare,
Hetzner,
Vercel,
Custom,
}
#[derive(Args, Debug)]
#[command(
about = "Inspect and manage DNS zones",
arg_required_else_help = true,
help_template = DNS_HELP_TEMPLATE,
after_help = DNS_ZONES_AFTER_HELP
)]
pub struct DnsZonesCmd {
#[command(subcommand)]
pub command: DnsZonesSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsZonesSubCommand {
#[command(about = "List zones for a provider account")]
List(DnsZoneListCmd),
#[command(about = "Fetch one zone by id")]
Get(DnsZoneGetCmd),
#[command(about = "Create a new zone")]
Create(DnsZoneCreateCmd),
#[command(about = "Edit zone-level properties")]
Edit(DnsZoneEditCmd),
#[command(about = "Delete a zone")]
Delete(DnsZoneDeleteCmd),
}
#[derive(Args, Debug)]
pub struct DnsZoneListCmd {
#[arg(long, value_enum, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
#[command(
about = "List, create, edit, import, export, and batch DNS records",
arg_required_else_help = true,
help_template = DNS_HELP_TEMPLATE,
after_help = DNS_RECORDS_AFTER_HELP
)]
pub struct DnsRecordsCmd {
#[command(subcommand)]
pub command: DnsRecordsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsRecordsSubCommand {
#[command(about = "List records in a zone")]
List(DnsRecordListCmd),
#[command(about = "Fetch one record by id")]
Get(DnsRecordGetCmd),
#[command(about = "Create a new DNS record")]
Create(DnsRecordCreateCmd),
#[command(about = "Replace a record by id with a full payload")]
Replace(DnsRecordReplaceCmd),
#[command(about = "Patch selected fields on a record")]
Edit(DnsRecordEditCmd),
#[command(about = "Delete a record")]
Delete(DnsRecordDeleteCmd),
#[command(about = "Apply a Cloudflare batch record payload from JSON")]
Batch(DnsRecordBatchCmd),
#[command(about = "Import a BIND-style zone file into a zone")]
Import(DnsRecordImportCmd),
#[command(about = "Export a zone as a BIND-style zone file")]
Export(DnsRecordExportCmd),
}
#[derive(Args, Debug)]
pub struct DnsRecordListCmd {
#[arg(long, value_enum, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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, default_value = "cloudflare")]
pub provider: DnsProviderKind,
#[arg(long)]
pub zone_id: String,
#[arg(long)]
pub output: Option<PathBuf>,
#[arg(long)]
pub token: Option<String>,
}
#[derive(Args, Debug)]
#[command(
about = "Inspect or edit DNSSEC status for a zone",
arg_required_else_help = true,
help_template = DNS_HELP_TEMPLATE,
after_help = DNS_DNSSEC_AFTER_HELP
)]
pub struct DnssecCmd {
#[command(subcommand)]
pub command: DnssecSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnssecSubCommand {
#[command(about = "Fetch DNSSEC state for a zone")]
Get(DnssecGetCmd),
#[command(about = "Edit DNSSEC-related flags for a zone")]
Edit(DnssecEditCmd),
}
#[derive(Args, Debug)]
pub struct DnssecGetCmd {
#[arg(long, value_enum, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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)]
#[command(
about = "Inspect or edit provider DNS settings for a zone",
arg_required_else_help = true,
help_template = DNS_HELP_TEMPLATE,
after_help = DNS_SETTINGS_AFTER_HELP
)]
pub struct DnsSettingsCmd {
#[command(subcommand)]
pub command: DnsSettingsSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum DnsSettingsSubCommand {
#[command(about = "Fetch provider DNS settings for a zone")]
Get(DnsSettingsGetCmd),
#[command(about = "Edit provider DNS settings for a zone")]
Edit(DnsSettingsEditCmd),
}
#[derive(Args, Debug)]
pub struct DnsSettingsGetCmd {
#[arg(long, value_enum, default_value = "cloudflare")]
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, default_value = "cloudflare")]
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)]
#[command(
arg_required_else_help = true,
disable_help_subcommand = true,
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::DOMAINS_AFTER_HELP
)]
pub struct DomainsCmd {
#[arg(long, value_enum, default_value = "cloudflare")]
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)]
#[command(
help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
after_help = crate::cli::help_render::DONE_AFTER_HELP
)]
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>,
}
#[derive(Args, Debug)]
pub struct FixProcessMonitorJsonCmd {
#[arg(help = "Path to a Cursor process-monitor JSON export")]
pub path: std::path::PathBuf,
#[arg(
long,
help = "Check whether the file needs repair without writing changes"
)]
pub check: bool,
#[arg(
long,
help = "Print repaired JSON to stdout instead of overwriting the file"
)]
pub stdout: bool,
}
#[derive(Args, Debug)]
pub struct CursorCmd {
#[command(subcommand)]
pub command: CursorSubCommand,
}
#[derive(Subcommand, Debug)]
pub enum CursorSubCommand {
#[command(about = "Upload local Cursor file history to the XBP dashboard")]
Ingest {
#[arg(
long,
help = "Scan local Cursor history without uploading to the dashboard"
)]
dry_run: bool,
},
}
#[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 {
/// Show complete addon status (enabled/disabled) from `microk8s status`
List,
/// Enable a MicroK8s addon
Enable {
#[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
name: String,
},
/// Disable a MicroK8s addon
Disable {
#[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
name: String,
},
}
#[cfg(feature = "kubernetes")]
#[derive(Subcommand, Debug)]
pub enum KubernetesSubCommand {
/// Validate kubectl, current context, and node readiness
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 Deployment/Service/NetworkPolicy YAML
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 a manifest bundle with kubectl apply -f
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,
},
/// Summarize deployments/services/pods in a namespace
Status {
#[arg(long, default_value = "default", help = "Namespace to summarize")]
namespace: String,
#[arg(long, help = "Override kube context")]
context: Option<String>,
},
/// Manage MicroK8s addons (list, enable, disable)
Addons(KubernetesAddonCmd),
/// Extract Kubernetes Dashboard login token from secret describe output
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>,
},
/// Print decoded Grafana admin credentials from observability secret
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>,
},
/// Create or update a cert-manager Issuer for Let's Encrypt
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 {
#[cfg(feature = "linear")]
use super::LinearConfigAction;
use super::{
Cli, CloudflareConfigAction, CloudflareJobWorkflow, CloudflareRollout,
CloudflareSubCommand, CloudflaredSubCommand, Commands, DnsProviderKind, DnsSubCommand,
DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand, GenerateSubCommand,
NetworkFloatingIpSubCommand, NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand,
NetworkSubCommand, SshCmd, VersionSubCommand, WorktreeWatchSubCommand,
};
#[cfg(feature = "secrets")]
use super::{
CloudflareSecretsSubCommand, 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, None);
}
_ => panic!("expected commit command"),
}
}
#[cfg(feature = "linear")]
#[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_cloudflare_doctor_with_app_after_subcommand() {
let cli = Cli::parse_from(["xbp", "cloudflare", "doctor", "--app", "auth"]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
assert!(matches!(
cloudflare_cmd.command,
CloudflareSubCommand::Doctor(_)
));
}
#[test]
fn parses_cloudflare_tunnel_run_with_token_and_log_level() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"tunnel",
"run",
"--token",
"tunnel-token",
"--log-level",
"debug",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
match cloudflare_cmd.command {
CloudflareSubCommand::Tunnel(tunnel_cmd) => match tunnel_cmd.command {
super::CloudflareTunnelSubCommand::Run(run_cmd) => {
assert_eq!(run_cmd.tunnel, None);
assert_eq!(run_cmd.token.as_deref(), Some("tunnel-token"));
assert_eq!(run_cmd.log_level.as_deref(), Some("debug"));
}
_ => panic!("expected tunnel run command"),
},
_ => panic!("expected cloudflare tunnel command"),
}
}
#[test]
fn parses_cloudflare_workflows_instance_command_with_local_flags() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"workflows",
"instances",
"send-event",
"my-workflow",
"latest",
"--type",
"my-event",
"--payload",
"{\"key\":\"value\"}",
"--local",
"--port",
"8787",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
match cloudflare_cmd.command {
CloudflareSubCommand::Workflows(workflows_cmd) => match workflows_cmd.command {
super::CloudflareWorkflowsSubCommand::External(args) => assert_eq!(
args,
[
"instances",
"send-event",
"my-workflow",
"latest",
"--type",
"my-event",
"--payload",
"{\"key\":\"value\"}",
"--local",
"--port",
"8787"
]
),
},
_ => panic!("expected cloudflare workflows command"),
}
}
#[test]
fn parses_cloudflare_env_dev_with_environment_and_port() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"env",
"dev",
"--env",
"staging",
"--port",
"8787",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
match cloudflare_cmd.command {
CloudflareSubCommand::Env(env_cmd) => match env_cmd.command {
super::CloudflareEnvSubCommand::External(args) => {
assert_eq!(args, ["dev", "--env", "staging", "--port", "8787"])
}
},
_ => panic!("expected cloudflare env command"),
}
}
#[test]
fn parses_cloudflare_release_with_domain_guard() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"release",
"--app",
"auth",
"--version",
"1.2.3",
"--rollout",
"gradual",
"--domain",
"auth-runtime",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
match cloudflare_cmd.command {
CloudflareSubCommand::Release(release_cmd) => {
assert_eq!(release_cmd.version, "1.2.3");
assert_eq!(release_cmd.rollout, CloudflareRollout::Gradual);
assert_eq!(release_cmd.domain.as_deref(), Some("auth-runtime"));
}
_ => panic!("expected release subcommand"),
}
}
#[test]
fn parses_cloudflare_deploy_parity_flags() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"deploy",
"--app",
"auth",
"--rollout",
"none",
"--skip-deploy",
"--allow-unchanged-container-image",
"--prune-old-images",
"--keep-image-tag-count",
"7",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
match cloudflare_cmd.command {
CloudflareSubCommand::Deploy(deploy_cmd) => {
assert_eq!(deploy_cmd.rollout, CloudflareRollout::None);
assert!(deploy_cmd.skip_deploy);
assert!(deploy_cmd.allow_unchanged_container_image);
assert!(deploy_cmd.prune_old_images);
assert_eq!(deploy_cmd.keep_image_tag_count, Some(7));
}
_ => panic!("expected deploy subcommand"),
}
}
#[test]
fn parses_cloudflare_release_parity_flags() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"release",
"--app",
"auth",
"--version",
"1.2.3",
"--rollout",
"none",
"--skip-deploy",
"--allow-unchanged-container-image",
"--prune-old-images",
"--keep-image-tag-count",
"5",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
match cloudflare_cmd.command {
CloudflareSubCommand::Release(release_cmd) => {
assert_eq!(release_cmd.rollout, CloudflareRollout::None);
assert!(release_cmd.skip_deploy);
assert!(release_cmd.allow_unchanged_container_image);
assert!(release_cmd.prune_old_images);
assert_eq!(release_cmd.keep_image_tag_count, Some(5));
}
_ => panic!("expected release subcommand"),
}
}
#[test]
fn parses_cloudflare_jobs_enqueue_with_workflow_payload() {
let cli = Cli::parse_from([
"xbp",
"cloudflare",
"jobs",
"enqueue",
"release",
"--app",
"auth",
"--version",
"1.2.3",
"--rollout",
"gradual",
"--skip-deploy",
"--prune-old-images",
]);
let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
panic!("expected cloudflare command");
};
assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
match cloudflare_cmd.command {
CloudflareSubCommand::Jobs(jobs_cmd) => match jobs_cmd.command {
super::CloudflareJobsSubCommand::Enqueue(enqueue_cmd) => {
assert_eq!(enqueue_cmd.workflow, CloudflareJobWorkflow::Release);
assert_eq!(enqueue_cmd.version.as_deref(), Some("1.2.3"));
assert_eq!(enqueue_cmd.rollout, CloudflareRollout::Gradual);
assert!(enqueue_cmd.skip_deploy);
assert!(enqueue_cmd.prune_old_images);
}
_ => panic!("expected enqueue subcommand"),
},
_ => panic!("expected jobs subcommand"),
}
}
#[test]
fn parses_worktree_watch_stop_force_command() {
let cli = Cli::parse_from([
"xbp",
"worktree-watch",
"stop",
"--repo",
"C:/Users/floris/Documents/GitHub/xbp",
"--force",
]);
let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
panic!("expected worktree-watch command");
};
match worktree_cmd.command {
WorktreeWatchSubCommand::Stop(stop_cmd) => {
assert_eq!(
stop_cmd.target.repo,
Some(PathBuf::from("C:/Users/floris/Documents/GitHub/xbp"))
);
assert!(stop_cmd.force);
}
_ => panic!("expected stop subcommand"),
}
}
#[test]
fn parses_worktree_watch_parent_detach_command() {
let cli = Cli::parse_from([
"xbp",
"worktree-watch",
"start",
"--parent",
"C:/Users/floris/Documents/GitHub",
"--detach",
]);
let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
panic!("expected worktree-watch command");
};
match worktree_cmd.command {
WorktreeWatchSubCommand::Start(start_cmd) => {
assert_eq!(
start_cmd.target.parent,
Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
);
assert!(start_cmd.detach);
}
_ => panic!("expected start subcommand"),
}
}
#[test]
fn parses_worktree_watch_status_repo_activity_command() {
let cli = Cli::parse_from([
"xbp",
"worktree-watch",
"status",
"--repo-activity",
"--stats-gap-minutes",
"30",
]);
let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
panic!("expected worktree-watch command");
};
match worktree_cmd.command {
WorktreeWatchSubCommand::Status(status_cmd) => {
assert!(status_cmd.repo_activity);
assert_eq!(status_cmd.stats_gap_minutes, 30);
}
_ => panic!("expected status subcommand"),
}
}
#[test]
fn parses_worktree_watch_tray_parent_command() {
let cli = Cli::parse_from([
"xbp",
"worktree-watch",
"tray",
"--parent",
"C:/Users/floris/Documents/GitHub",
"--sync-interval-seconds",
"90",
]);
let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
panic!("expected worktree-watch command");
};
match worktree_cmd.command {
WorktreeWatchSubCommand::Tray(tray_cmd) => {
assert_eq!(
tray_cmd.target.parent,
Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
);
assert!(tray_cmd.target.repos.is_empty());
assert_eq!(tray_cmd.sync_interval_seconds, 90);
}
_ => panic!("expected tray subcommand"),
}
}
#[test]
fn parses_worktree_watch_tray_foreground_command() {
let cli = Cli::parse_from([
"xbp",
"worktree-watch",
"tray",
"--foreground",
"--parent",
"C:/Users/floris/Documents/GitHub",
]);
let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
panic!("expected worktree-watch command");
};
match worktree_cmd.command {
WorktreeWatchSubCommand::Tray(tray_cmd) => {
assert!(tray_cmd.foreground);
assert_eq!(
tray_cmd.target.parent,
Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
);
}
_ => panic!("expected tray subcommand"),
}
}
#[test]
fn parses_worktree_watch_tray_repos_command() {
let cli = Cli::parse_from([
"xbp",
"worktree-watch",
"tray",
"--repos",
"C:/src/a",
"C:/src/b",
]);
let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
panic!("expected worktree-watch command");
};
match worktree_cmd.command {
WorktreeWatchSubCommand::Tray(tray_cmd) => {
assert_eq!(
tray_cmd.target.repos,
vec![PathBuf::from("C:/src/a"), PathBuf::from("C:/src/b")]
);
}
_ => panic!("expected tray subcommand"),
}
}
#[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"));
assert!(run_cmd.auto_fix);
}
_ => panic!("expected publish run"),
}
}
_ => panic!("expected workspace publish"),
}
}
_ => panic!("expected version workspace command"),
}
}
#[test]
fn parses_version_workspace_publish_plan_with_only_and_include_prereqs() {
let cli = Cli::parse_from([
"xbp",
"version",
"workspace",
"publish",
"plan",
"--repo",
"C:/Users/floris/Documents/GitHub/athena-auth",
"--only",
"athena-auth",
"--include-prereqs",
]);
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::Plan(plan_cmd) => {
assert_eq!(
plan_cmd.target.repo,
Some(PathBuf::from(
"C:/Users/floris/Documents/GitHub/athena-auth"
))
);
assert_eq!(plan_cmd.only.as_deref(), Some("athena-auth"));
assert!(plan_cmd.include_prereqs);
}
_ => panic!("expected publish plan"),
}
}
_ => 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_global_push_flag() {
let cli = Cli::parse_from(["xbp", "--push", "version", "patch"]);
assert!(cli.push);
let Some(Commands::Version(version_cmd)) = cli.command else {
panic!("expected version command");
};
assert_eq!(version_cmd.target.as_deref(), Some("patch"));
}
#[test]
fn parses_version_push_flag() {
let cli = Cli::parse_from(["xbp", "version", "patch", "--push"]);
assert!(cli.push);
let Some(Commands::Version(version_cmd)) = cli.command else {
panic!("expected version command");
};
assert!(version_cmd.push);
}
#[test]
fn parses_version_bump_push_flag() {
let cli = Cli::parse_from(["xbp", "version", "bump", "--all", "--patch", "-p"]);
let Some(Commands::Version(version_cmd)) = cli.command else {
panic!("expected version command");
};
let Some(VersionSubCommand::Bump(bump_cmd)) = version_cmd.command else {
panic!("expected version bump subcommand");
};
assert!(bump_cmd.push);
assert!(bump_cmd.all);
assert!(bump_cmd.patch);
}
#[test]
fn parses_version_alias_release_alias() {
let cli = Cli::parse_from([
"xbp",
"v",
"r",
"--draft",
"--publish",
"--force",
"--flag",
"nightly",
]);
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);
assert!(release_cmd.publish);
assert!(release_cmd.force);
assert_eq!(release_cmd.flag, Some(super::VersionReleaseFlag::Nightly));
}
#[test]
fn parses_version_domain_release_guard_flags() {
let cli = Cli::parse_from([
"xbp",
"version",
"domain",
"release",
"--domain",
"auth-runtime",
"--version",
"2.0.0",
"--deploy",
"--allow-major-jump",
"--allow-cross-domain-version",
"platform",
]);
let Some(Commands::Version(version_cmd)) = cli.command else {
panic!("expected version command");
};
let Some(super::VersionSubCommand::Domain(domain_cmd)) = version_cmd.command else {
panic!("expected domain command");
};
match domain_cmd.command {
super::VersionDomainSubCommand::Release(release_cmd) => {
assert_eq!(release_cmd.domain, "auth-runtime");
assert_eq!(release_cmd.version.as_deref(), Some("2.0.0"));
assert!(release_cmd.deploy);
assert!(release_cmd.allow_major_jump);
assert_eq!(
release_cmd.allow_cross_domain_version.as_deref(),
Some("platform")
);
}
_ => panic!("expected domain release"),
}
}
#[test]
fn parses_publish_command_target_filter() {
let cli = Cli::parse_from([
"xbp",
"publish",
"--allow-dirty",
"--force",
"--include-prereqs",
"--target",
"npm",
"--service",
"web",
"--manifest-path",
"apps/web/package.json",
]);
let Some(Commands::Publish(publish_cmd)) = cli.command else {
panic!("expected publish command");
};
assert!(publish_cmd.allow_dirty);
assert!(publish_cmd.force);
assert!(publish_cmd.include_prereqs);
assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
assert_eq!(publish_cmd.service.as_deref(), Some("web"));
assert_eq!(
publish_cmd.manifest_path,
Some(PathBuf::from("apps/web/package.json"))
);
}
#[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_release_setup_config_command() {
let cli = Cli::parse_from(["xbp", "config", "release", "setup"]);
let Some(Commands::Config(config_cmd)) = cli.command else {
panic!("expected config command");
};
let Some(super::ConfigProviderCmd::Release(release_cmd)) = config_cmd.provider else {
panic!("expected release config command");
};
assert!(matches!(
release_cmd.action,
super::ReleaseConfigAction::Setup
));
}
#[test]
fn parses_crates_login_config_command() {
let cli = Cli::parse_from(["xbp", "config", "crates", "login"]);
let Some(Commands::Config(config_cmd)) = cli.command else {
panic!("expected config command");
};
let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
panic!("expected crates config command");
};
assert!(matches!(
crates_cmd.action,
super::CratesConfigAction::Login { .. }
));
}
#[test]
fn parses_crates_logout_config_command() {
let cli = Cli::parse_from(["xbp", "config", "crates", "logout"]);
let Some(Commands::Config(config_cmd)) = cli.command else {
panic!("expected config command");
};
let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
panic!("expected crates config command");
};
assert!(matches!(
crates_cmd.action,
super::CratesConfigAction::Logout
));
}
#[test]
fn parses_whoami_command() {
let cli = Cli::parse_from(["xbp", "whoami"]);
assert!(matches!(cli.command, Some(Commands::Whoami)));
}
#[test]
fn parses_update_command_with_flags() {
let cli = Cli::parse_from([
"xbp",
"update",
"--json",
"--install",
"--fail-if-outdated",
"--crate",
"xbp",
]);
let Some(Commands::Update(cmd)) = cli.command else {
panic!("expected update command");
};
assert!(cmd.json);
assert!(cmd.install);
assert!(cmd.fail_if_outdated);
assert_eq!(cmd.crate_name, "xbp");
}
#[test]
fn parses_upgrade_alias_as_update() {
let cli = Cli::parse_from(["xbp", "upgrade"]);
assert!(matches!(cli.command, Some(Commands::Update(_))));
}
#[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_eq!(secrets_cmd.environment, "xbp-dev");
}
_ => 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_eq!(secrets_cmd.environment, "xbp-prod");
assert!(matches!(
secrets_cmd.command,
Some(SecretsSubCommand::Push(_))
));
}
_ => panic!("expected secrets command"),
}
}
#[cfg(feature = "secrets")]
#[test]
fn parses_secrets_dev_vars_sync_flags() {
let cli = Cli::parse_from([
"xbp",
"secrets",
"--environment",
"xbp-prod",
"pull",
"--include-dev-vars",
"--dev-vars-output",
".dev.vars",
]);
match cli.command {
Some(Commands::Secrets(secrets_cmd)) => {
assert_eq!(secrets_cmd.environment, "xbp-prod");
match secrets_cmd.command {
Some(SecretsSubCommand::Pull(pull_cmd)) => {
assert!(pull_cmd.include_dev_vars);
assert_eq!(pull_cmd.dev_vars_output.as_deref(), Some(".dev.vars"));
}
_ => panic!("expected pull command"),
}
}
_ => panic!("expected secrets command"),
}
}
#[test]
fn parses_version_discover_command() {
let cli = Cli::parse_from(["xbp", "version", "discover", "--dry-run"]);
match cli.command {
Some(Commands::Version(version_cmd)) => match version_cmd.command {
Some(super::VersionSubCommand::Discover(discover_cmd)) => {
assert!(discover_cmd.dry_run);
assert!(!discover_cmd.no_register);
}
_ => panic!("expected version discover subcommand"),
},
_ => panic!("expected version 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_workers_secret_put_from_stdin_command() {
let cli = Cli::parse_from([
"xbp",
"workers",
"secrets",
"--environment",
"production",
"put",
"--name",
"API_KEY",
"--from-stdin",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::Secrets(secrets_cmd) => {
assert_eq!(
secrets_cmd.target.environment.as_deref(),
Some("production")
);
match secrets_cmd.command {
super::WorkersSecretsSubCommand::Put(put_cmd) => {
assert_eq!(put_cmd.name, "API_KEY");
assert!(put_cmd.from_stdin);
assert_eq!(put_cmd.value, None);
}
_ => panic!("expected workers secret put"),
}
}
_ => panic!("expected workers secrets command"),
}
}
#[test]
fn parses_workers_d1_migrations_local_command() {
let cli = Cli::parse_from([
"xbp",
"workers",
"d1",
"migrations",
"apply",
"DB",
"--local",
"--environment",
"preview",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
super::WorkersD1SubCommand::Migrations(migrations_cmd) => {
match migrations_cmd.command {
super::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
assert_eq!(apply_cmd.database, "DB");
assert!(apply_cmd.local);
assert!(!apply_cmd.remote);
assert_eq!(apply_cmd.target.environment.as_deref(), Some("preview"));
}
}
}
},
_ => panic!("expected workers d1 command"),
}
}
#[test]
fn parses_workers_wrangler_passthrough_for_r2_queues_and_secrets_store() {
let cli = Cli::parse_from([
"xbp",
"workers",
"wrangler",
"run",
"--",
"r2",
"bucket",
"cors",
"set",
"r2-bucket",
"--file",
"cors.json",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::Wrangler(wrangler_cmd) => match wrangler_cmd.command {
super::WorkersWranglerSubCommand::Run(run_cmd) => assert_eq!(
run_cmd.args,
[
"r2",
"bucket",
"cors",
"set",
"r2-bucket",
"--file",
"cors.json"
]
),
_ => panic!("expected Wrangler run command"),
},
_ => panic!("expected workers wrangler command"),
}
}
#[test]
fn parses_workers_deploy_ci_version_upload_command() {
let cli = Cli::parse_from(["xbp", "workers", "deploy", "ci", "--version-upload"]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
super::WorkersDeploySubCommand::Ci(ci_cmd) => {
assert!(ci_cmd.version_upload);
}
_ => panic!("expected workers deploy ci command"),
},
_ => panic!("expected workers deploy command"),
}
}
#[test]
fn parses_workers_list_alias_command() {
let cli = Cli::parse_from(["xbp", "workers", "ls", "--all"]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::List(list_cmd) => {
assert!(list_cmd.all);
assert!(!list_cmd.json);
}
_ => panic!("expected workers list command"),
}
}
#[test]
fn parses_workers_logs_follow_and_build_flags() {
let cli = Cli::parse_from([
"xbp",
"workers",
"logs",
"-f",
"--build",
"--failed",
"xbp-production",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::Logs(logs_cmd) => {
assert!(logs_cmd.follow);
assert!(logs_cmd.build);
assert!(logs_cmd.failed);
assert_eq!(logs_cmd.script_name.as_deref(), Some("xbp-production"));
}
_ => panic!("expected workers logs command"),
}
}
#[test]
fn parses_workers_logs_worker_and_wait_flags() {
let cli = Cli::parse_from([
"xbp",
"workers",
"logs",
"--worker",
"suits-formations",
"--json",
"--wait",
"--wait-seconds",
"60",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::Logs(logs_cmd) => {
assert_eq!(logs_cmd.target.worker.as_deref(), Some("suits-formations"));
assert!(logs_cmd.json);
assert!(logs_cmd.wait);
assert_eq!(logs_cmd.wait_seconds, 60);
}
_ => panic!("expected workers logs command"),
}
}
#[test]
fn parses_workers_logs_output_and_filter_flags() {
let cli = Cli::parse_from([
"xbp",
"workers",
"logs",
"--build",
"-n",
"200",
"-o",
"build.log",
"-g",
"error",
"--errors-only",
"--list-builds",
"--build-index",
"1",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::Logs(logs_cmd) => {
assert!(logs_cmd.build);
assert_eq!(logs_cmd.lines, Some(200));
assert_eq!(
logs_cmd.output.as_deref().and_then(|path| path.to_str()),
Some("build.log")
);
assert_eq!(logs_cmd.grep.as_deref(), Some("error"));
assert!(logs_cmd.errors_only);
assert!(logs_cmd.list_builds);
assert_eq!(logs_cmd.build_index, Some(1));
}
_ => panic!("expected workers logs command"),
}
}
#[test]
fn parses_workers_list_filter_and_limit_flags() {
let cli = Cli::parse_from([
"xbp", "workers", "list", "--failed", "-n", "5", "--sort", "modified",
]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command");
};
match workers_cmd.command {
super::WorkersSubCommand::List(list_cmd) => {
assert!(list_cmd.failed);
assert_eq!(list_cmd.limit, Some(5));
assert_eq!(list_cmd.sort, "modified");
}
_ => panic!("expected workers list command"),
}
}
#[test]
fn parses_worker_alias_command() {
let cli = Cli::parse_from(["xbp", "worker", "env", "--show-values"]);
let Some(Commands::Workers(workers_cmd)) = cli.command else {
panic!("expected workers command through alias");
};
match workers_cmd.command {
super::WorkersSubCommand::Env(env_cmd) => {
assert!(env_cmd.show_values);
}
_ => panic!("expected workers env 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 dns_zone_list_defaults_provider_to_cloudflare() {
let cli = Cli::parse_from(["xbp", "dns", "zones", "list"]);
let Some(Commands::Dns(dns_cmd)) = cli.command else {
panic!("expected dns command");
};
match dns_cmd.command {
DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
DnsZonesSubCommand::List(list_cmd) => {
assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
}
_ => panic!("expected zones list command"),
},
_ => panic!("expected zones command"),
}
}
#[test]
fn dns_record_list_defaults_provider_to_cloudflare() {
let cli = Cli::parse_from(["xbp", "dns", "records", "list", "--zone-id", "zone_123"]);
let Some(Commands::Dns(dns_cmd)) = cli.command else {
panic!("expected dns command");
};
match dns_cmd.command {
DnsSubCommand::Records(records_cmd) => match records_cmd.command {
super::DnsRecordsSubCommand::List(list_cmd) => {
assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
assert_eq!(list_cmd.zone_id, "zone_123");
}
_ => panic!("expected records list command"),
},
_ => panic!("expected records command"),
}
}
#[test]
fn dns_help_includes_descriptions_and_examples() {
let err = Cli::try_parse_from(["xbp", "dns", "-h"]).expect_err("help");
let rendered = err.to_string();
assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
assert!(rendered.contains("Manage DNS providers, zones, records, DNSSEC, and settings"));
assert!(rendered.contains("List supported DNS providers and current implementation status"));
assert!(rendered.contains("xbp dns records create"));
}
#[test]
fn dns_providers_help_includes_discovery_note() {
let err = Cli::try_parse_from(["xbp", "dns", "providers", "-h"]).expect_err("help");
let rendered = err.to_string();
assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
assert!(rendered.contains("Implemented providers are wired into `xbp dns` today."));
}
#[test]
fn dns_records_without_subcommand_displays_help_screen() {
let err = Cli::try_parse_from(["xbp", "dns", "records"]).expect_err("missing subcommand");
let rendered = err.to_string();
assert!(matches!(
err.kind(),
clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
| clap::error::ErrorKind::MissingSubcommand
));
assert!(rendered.contains("List, create, edit, import, export, and batch DNS records"));
assert!(rendered.contains("Create a new DNS record"));
assert!(rendered.contains("xbp dns records import"));
}
#[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",
"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,
Some(CloudflareConfigAction::SetAccountId { .. })
));
}
_ => panic!("expected cloudflare config provider"),
},
_ => panic!("expected config command"),
}
}
#[test]
fn parses_runners_groups_update_command() {
let cli = Cli::parse_from([
"xbp",
"runners",
"groups",
"update",
"--organization-id",
"org_123",
"--name",
"linux-builders",
"--visibility",
"selected",
"--restricted-to-workflows",
]);
let Some(Commands::Runners(runners_cmd)) = cli.command else {
panic!("expected runners command");
};
match runners_cmd.command {
super::RunnersSubCommand::Groups(groups_cmd) => match groups_cmd.command {
super::RunnersGroupsSubCommand::Update(update_cmd) => {
assert_eq!(update_cmd.organization_id, "org_123");
assert_eq!(update_cmd.name, "linux-builders");
assert_eq!(update_cmd.visibility.as_deref(), Some("selected"));
assert!(update_cmd.restricted_to_workflows);
}
_ => panic!("expected runners groups update command"),
},
_ => panic!("expected runners groups command"),
}
}
#[test]
fn parses_api_runners_group_repositories_create_command() {
let cli = Cli::parse_from([
"xbp",
"api",
"runners",
"group-repositories-create",
"group_123",
"--repository-owner",
"xylex-group",
"--repository-name",
"xbp",
"--repository-full-name",
"xylex-group/xbp",
"--is-private",
]);
let Some(Commands::Api(api_cmd)) = cli.command else {
panic!("expected api command");
};
match api_cmd.command {
super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
super::ApiRunnersSubCommand::GroupRepositoriesCreate(create_cmd) => {
assert_eq!(create_cmd.runner_group_id, "group_123");
assert_eq!(create_cmd.repository_owner, "xylex-group");
assert_eq!(create_cmd.repository_name, "xbp");
assert_eq!(create_cmd.repository_full_name, "xylex-group/xbp");
assert!(create_cmd.is_private);
}
_ => panic!("expected api runners group repositories create command"),
},
_ => panic!("expected api runners command"),
}
}
#[test]
fn parses_runners_host_preflight_command() {
let cli = Cli::parse_from([
"xbp",
"runners",
"hosts",
"preflight",
"--runner-host-id",
"host_123",
"--write-api",
]);
let Some(Commands::Runners(runners_cmd)) = cli.command else {
panic!("expected runners command");
};
match runners_cmd.command {
super::RunnersSubCommand::Hosts(hosts_cmd) => match hosts_cmd.command {
super::RunnersHostsSubCommand::Preflight(preflight_cmd) => {
assert_eq!(preflight_cmd.runner_host_id, "host_123");
assert!(preflight_cmd.write_api);
}
_ => panic!("expected runners hosts preflight command"),
},
_ => panic!("expected runners hosts command"),
}
}
#[test]
fn parses_runners_agent_serve_command() {
let cli = Cli::parse_from([
"xbp",
"runners",
"agent",
"serve",
"--runner-host-id",
"host_123",
"--interval-seconds",
"5",
"--once",
]);
let Some(Commands::Runners(runners_cmd)) = cli.command else {
panic!("expected runners command");
};
match runners_cmd.command {
super::RunnersSubCommand::Agent(agent_cmd) => match agent_cmd.command {
super::RunnersAgentSubCommand::Serve(serve_cmd) => {
assert_eq!(serve_cmd.runner_host_id, "host_123");
assert_eq!(serve_cmd.interval_seconds, 5);
assert!(serve_cmd.once);
}
},
_ => panic!("expected runners agent command"),
}
}
#[test]
fn parses_api_runners_host_preflight_upsert_command() {
let cli = Cli::parse_from([
"xbp",
"api",
"runners",
"hosts-preflights-upsert",
"--runner-host-id",
"host_123",
"--platform",
"linux",
"--status",
"synced",
"--service-manager",
"systemd",
]);
let Some(Commands::Api(api_cmd)) = cli.command else {
panic!("expected api command");
};
match api_cmd.command {
super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
super::ApiRunnersSubCommand::HostsPreflightsUpsert(upsert_cmd) => {
assert_eq!(upsert_cmd.runner_host_id, "host_123");
assert_eq!(upsert_cmd.platform, "linux");
assert_eq!(upsert_cmd.status, "synced");
assert_eq!(upsert_cmd.service_manager.as_deref(), Some("systemd"));
}
_ => panic!("expected api runners host preflight upsert command"),
},
_ => panic!("expected api runners command"),
}
}
#[cfg(feature = "linear")]
#[test]
fn parses_linear_list_and_todos_sync() {
let cli = Cli::parse_from([
"xbp",
"linear",
"list",
"--team",
"XLX",
"--assignee",
"me",
"--limit",
"10",
]);
let Some(Commands::Linear(cmd)) = cli.command else {
panic!("expected linear command");
};
match cmd.command {
Some(super::LinearSubCommand::List(list)) => {
assert_eq!(list.team.as_deref(), Some("XLX"));
assert_eq!(list.assignee.as_deref(), Some("me"));
assert_eq!(list.limit, 10);
}
_ => panic!("expected linear list"),
}
let cli = Cli::parse_from([
"xbp",
"todos",
"sync",
"--to",
"linear",
"--dry-run",
"--yes",
"--team",
"XLX",
]);
let Some(Commands::Todos(cmd)) = cli.command else {
panic!("expected todos command");
};
match cmd.command {
Some(super::TodosSubCommand::Sync(sync)) => {
assert_eq!(sync.to, Some(super::TodosSyncTarget::Linear));
assert!(sync.dry_run);
assert!(sync.yes);
assert_eq!(sync.team.as_deref(), Some("XLX"));
}
_ => panic!("expected todos sync"),
}
let cli = Cli::parse_from([
"xbp",
"issues",
"sync",
"--to",
"linear",
"--dry-run",
"--yes",
"--team",
"XLX",
]);
let Some(Commands::Issues(cmd)) = cli.command else {
panic!("expected issues command");
};
match cmd.command {
Some(super::TodosSubCommand::Sync(sync)) => {
assert_eq!(sync.to, Some(super::TodosSyncTarget::Linear));
assert!(sync.dry_run);
assert!(sync.yes);
assert_eq!(sync.team.as_deref(), Some("XLX"));
}
_ => panic!("expected issues sync"),
}
let cli = Cli::parse_from(["xbp", "gh", "show", "42"]);
let Some(Commands::Github(cmd)) = cli.command else {
panic!("expected github command");
};
match cmd.command {
Some(super::GithubSubCommand::Show(show)) => {
assert_eq!(show.id, "42");
}
_ => panic!("expected github show"),
}
}
#[cfg(feature = "linear")]
#[test]
fn parses_linear_search_with_tui() {
let cli = Cli::parse_from([
"xbp", "linear", "search", "cache", "--team", "XLX", "--state", "done", "--sort",
"priority", "--tui",
]);
let Some(Commands::Linear(cmd)) = cli.command else {
panic!("expected linear command");
};
match cmd.command {
Some(super::LinearSubCommand::Search(search)) => {
assert_eq!(search.query, "cache");
assert_eq!(search.team.as_deref(), Some("XLX"));
assert_eq!(search.state.as_deref(), Some("done"));
assert_eq!(search.sort, "priority");
assert!(search.tui);
}
_ => panic!("expected linear search"),
}
}
#[test]
fn parses_singular_issue_search() {
let cli = Cli::parse_from([
"xbp",
"issue",
"search",
"cache",
"--github",
"--owner",
"xylex-group",
"--repo",
"xbp",
"--limit",
"25",
]);
let Some(Commands::Issues(cmd)) = cli.command else {
panic!("expected issue command");
};
match cmd.command {
Some(super::TodosSubCommand::Search(search)) => {
assert_eq!(search.query, "cache");
assert!(search.github);
assert_eq!(search.owner.as_deref(), Some("xylex-group"));
assert_eq!(search.repo.as_deref(), Some("xbp"));
assert_eq!(search.limit, 25);
}
_ => panic!("expected issue search"),
}
}
#[cfg(not(feature = "linear"))]
#[test]
fn parses_github_show_without_linear_feature() {
let cli = Cli::parse_from(["xbp", "gh", "show", "42"]);
let Some(Commands::Github(cmd)) = cli.command else {
panic!("expected github command");
};
match cmd.command {
Some(super::GithubSubCommand::Show(show)) => {
assert_eq!(show.id, "42");
}
_ => panic!("expected github show"),
}
}
}