1use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Parser, Debug)]
6#[command(
7 name = "xbp",
8 version,
9 disable_version_flag = true,
11 about = "Deploy, operate, and debug services with one CLI.",
12 long_about = "XBP is an operations-first CLI for deployments, diagnostics, service orchestration,\nnetwork controls, and runtime observability.",
13 disable_help_subcommand = false,
14 next_line_help = true,
15 help_template = crate::cli::help_render::XBP_ROOT_HELP_TEMPLATE,
16 after_help = crate::cli::help_render::XBP_ROOT_AFTER_HELP
17)]
18pub struct Cli {
19 #[arg(
21 short = 'v',
23 long = "version",
24 visible_short_alias = 'V',
25 action = ArgAction::Version
26 )]
27 print_version: Option<bool>,
28 #[arg(
30 short = 'q',
31 long = "query",
32 value_name = "SEARCH_QUERY",
33 help = "Search the CLI command tree (names, aliases, about, flags)"
34 )]
35 pub query: Option<String>,
36 #[arg(long, global = true, help = "Enable verbose debugging output")]
37 pub debug: bool,
38 #[arg(
39 long = "push",
40 global = true,
41 help = "Push after auto-committing generated changes (also honors `github.auto_push_on_commit` when omitted)"
42 )]
43 pub push: bool,
44 #[arg(short = 'l', help = "List pm2 processes")]
45 pub list: bool,
46 #[arg(short = 'p', long = "port", help = "Filter by port number")]
47 pub port: Option<u16>,
48 #[arg(long, help = "Open logs directory")]
49 pub logs: bool,
50 #[arg(long, help = "Print the complete alphabetical command reference")]
51 pub commands: bool,
52
53 #[command(subcommand)]
54 pub command: Option<Commands>,
55}
56
57#[derive(Subcommand, Debug)]
58pub enum Commands {
59 #[command(about = "Inspect or manage listening ports")]
60 Ports(PortsCmd),
61 #[command(
62 about = "Analyze the current git worktree and create a conventional commit",
63 visible_alias = "c"
64 )]
65 Commit(CommitCmd),
66 #[command(
67 about = "Sync with remote (stash-safe rebase when behind/diverged) and push the current branch",
68 long_about = "Automates the recover-and-push flow when plain git fails:\n\
69 • non-fast-forward push rejection\n\
70 • diverging branches under pull.ff=only\n\
71 • dirty unstaged/untracked WIP blocking rebase\n\n\
72Steps: fetch → stash dirty WIP if needed → pull --rebase → push → restore stash.\n\
73Does not create commits; use `xbp commit --push` to commit local changes first.",
74 after_help = "Examples:\n xbp push\n xbp commit --push # commit dirty files, then same sync+push path"
75 )]
76 Push,
77 #[command(
78 about = "Plan/run/verify service deploys from services[] (groups via deploy.groups)",
79 long_about = "Service-first deploy orchestration.\n\n\
80Targets (optional in a TTY — interactive picker):\n\
81 • service name from services[]\n\
82 • deploy.groups.<name> (ordered multi-service)\n\
83 • all (every service with deploy.envs.<env>)\n\n\
84Modes (flags; omit for interactive picker in a TTY, else --plan):\n\
85 --plan print plan only (default when no other mode flag)\n\
86 --run apply providers (kubernetes, kubernetes-operator, worker, …)\n\
87 --verify read-only rollout/health checks\n\
88 --status status snapshot\n\
89 --promote promotion label (runs apply path)\n\
90 --history list recent deploy records under .xbp/deployments\n\n\
91Bare `xbp deploy` opens an interactive wizard (target → mode → env).\n\
92Missing deploy.envs.<env> still triggers the setup wizard before planning.\n\
93Does not use product islands like `xbp athena deploy`.\n\
94`--promote TAG` retags OCI images by digest (no rebuild); combine with `--run` to also apply.",
95 after_help = "Examples:\n xbp deploy\n xbp deploy --plan\n xbp deploy xbp-github-runner --plan\n xbp deploy athena --env production --run --yes\n xbp deploy all --env production --plan\n xbp deploy workers --history\n xbp deploy athena --promote stable --dry-run"
96 )]
97 Deploy(DeployCmd),
98 #[command(
99 about = "OCI registry primitives (inspect, tags, digest, promote)",
100 long_about = "Generic OCI registry operations used by deploy planning and promotion.\n\
101Auth: --username/--token, env (GH_TOKEN, GITHUB_TOKEN, XBP_GITHUB_TOKEN, XBP_OCI_*),\n\
102global GitHub token, xbp config oci registry tokens, or project oci.registries.",
103 after_help = "Examples:\n xbp oci inspect ghcr.io/xylex-group/athena:4.0.1 --json\n xbp oci digest ghcr.io/xylex-group/athena:4.0.1\n xbp oci tags ghcr.io/xylex-group/athena\n xbp oci promote ghcr.io/xylex-group/athena:4.0.1 --to stable --dry-run"
104 )]
105 Oci(OciCmd),
106 #[command(
107 about = "Initialize an XBP project and run the full setup wizard",
108 long_about = "Creates `.xbp/xbp.toml` from project detection (or opens the wizard when a project config already exists), then walks through:\n\
109 • version targets & release scope\n\
110 • GitHub release branch template + auto-push\n\
111 • Linear release posts (API key + initiative picker)\n\
112 • cargo-dist packaging\n\
113 • crates.io / npm publish targets\n\
114 • Cloudflare Workers registration\n\
115 • OCI registry & Kubernetes defaults\n\
116 • deploy groups, Discord webhooks, monitors\n\
117Re-run in a nested package folder to register a service.\n\
118Re-run at the project root to reopen the setup wizard without overwriting."
119 )]
120 Init,
121 #[command(about = "Install common dependencies for host setup")]
122 Setup,
123 #[command(about = "Redeploy one service or the entire project")]
124 Redeploy {
125 #[arg(
126 help = "Service name to redeploy (optional, uses legacy redeploy.sh if not provided)"
127 )]
128 service_name: Option<String>,
129 },
130 #[command(about = "Run the legacy remote redeploy workflow over SSH")]
131 RedeployV2(RedeployV2Cmd),
132 #[command(about = "Inspect project/global config and manage provider keys")]
133 Config(ConfigCmd),
134 #[command(
135 about = "Install supported host packages or project tooling",
136 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
137 after_help = crate::commands::INSTALL_COMMAND_AFTER_HELP
138 )]
139 Install {
140 #[arg(short = 'l', long = "list", help = "List installable targets and exit")]
141 list: bool,
142 #[arg(
143 long = "force",
144 help = "Allow an explicitly selected install method to run even when it does not match the current OS"
145 )]
146 force: bool,
147 #[arg(help = "Install target (leave empty to show installable options)")]
148 package: Option<String>,
149 },
150 #[command(about = "Tail local or remote logs")]
151 Logs(LogsCmd),
152 #[command(
153 about = "Open an interactive remote shell over SSH",
154 visible_alias = "shell"
155 )]
156 Ssh(SshCmd),
157 #[command(about = "Open or manage cloudflared TCP forwarders")]
158 Cloudflared(CloudflaredCmd),
159 #[command(about = "List PM2 processes")]
160 List,
161 #[command(about = "Fetch an HTTP endpoint with sane defaults")]
162 Curl(CurlCmd),
163 #[command(about = "List configured services from project config")]
164 Services,
165 #[command(
166 about = "Run service commands with interactive preview/picker when no args given. Stores run history + registry under global ~/.xbp/logs/service/"
167 )]
168 Service {
169 #[arg(help = "Command (build/install/start/dev/pre). Omit to use interactive picker.")]
170 command: Option<String>,
171 #[arg(help = "Service name (omit for picker)")]
172 service_name: Option<String>,
173 },
174 #[command(about = "Manage NGINX site configs and upstream mappings")]
175 Nginx(NginxCmd),
176 #[command(about = "Manage host network configuration and floating IPs")]
177 Network(NetworkCmd),
178 #[command(about = "Run full system diagnostics and readiness checks")]
179 Diag(DiagCmd),
180 #[command(about = "Run health-check monitoring commands")]
181 Monitor(MonitorCmd),
182 #[command(about = "Capture a PM2 snapshot for later restore")]
183 Snapshot,
184 #[command(about = "Restore PM2 state from dump or latest snapshot")]
185 Resurrect,
186 #[command(about = "Stop a PM2 process by name or stop all")]
187 Stop {
188 #[arg(help = "PM2 process name or 'all' (default: all)")]
189 target: Option<String>,
190 },
191 #[command(about = "Flush PM2 logs globally or for a specific process")]
192 Flush {
193 #[arg(help = "Optional PM2 process name")]
194 target: Option<String>,
195 },
196 #[command(about = "Run or inspect the CLI login flow against the XBP dashboard")]
197 Login(LoginCmd),
198 #[command(about = "Show the current signed-in CLI identity")]
199 Whoami,
200 #[command(
201 about = "Check crates.io for a newer XBP CLI release (and optionally install it)",
202 visible_alias = "upgrade",
203 after_help = "Examples:\n xbp update\n xbp update --json\n xbp update --install\n xbp update --install --features all\n xbp update --install --features secrets,linear,docker\n xbp update --install --no-default-features --features secrets\n xbp update --fail-if-outdated\n xbp update --crate xbp\n\n`xbp update --install` defaults to `--features all` (secrets, linear, docker, kubernetes, openapi-gen, athena, monitoring, nordvpn, systemd; not kafka)."
204 )]
205 Update(UpdateCmd),
206 #[command(
207 about = "Inspect, reconcile, or bump project versions",
208 visible_alias = "v"
209 )]
210 Version(VersionCmd),
211 #[command(about = "Run configured npm/crates publish workflows for the current XBP project")]
212 Publish(PublishCmd),
213 #[command(about = "Show PM2 environment by name or numeric id")]
214 Env {
215 #[arg(help = "PM2 process name or id")]
216 target: String,
217 },
218 #[command(about = "Tail app logs or Kafka logs")]
219 Tail(TailCmd),
220 #[command(about = "Start a binary/process under PM2")]
221 Start {
222 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
223 args: Vec<String>,
224 },
225 #[command(about = "Generate helper artifacts such as systemd units")]
226 Generate(GenerateCmd),
227 #[cfg(feature = "secrets")]
228 #[command(about = "Manage env vars and GitHub Actions environment variables (feature-gated)")]
229 Secrets(SecretsCmd),
230 #[command(
231 about = "Manage Cloudflare Workers secrets, Wrangler config helpers, D1 migrations, and deploy flows",
232 visible_alias = "worker"
233 )]
234 Workers(WorkersCmd),
235 #[command(about = "Run canonical Cloudflare Worker + Container workflows for an XBP project")]
236 Cloudflare(CloudflareCmd),
237 #[command(about = "Manage DNS providers, zones, records, DNSSEC, and settings")]
238 Dns(DnsCmd),
239 #[command(about = "Discover and inspect registered domains")]
240 Domains(DomainsCmd),
241 #[command(
242 about = "Generate 'what did I get done' Markdown report from git commits across repos"
243 )]
244 Done(DoneCmd),
245 #[command(
246 about = "Repair malformed Cursor process-monitor JSON exports",
247 visible_alias = "fix-pm-json"
248 )]
249 FixProcessMonitorJson(FixProcessMonitorJsonCmd),
250 #[command(about = "Upload Cursor local file history to the XBP dashboard")]
251 Cursor(CursorCmd),
252 #[cfg(feature = "kubernetes")]
253 #[command(
254 about = "Kubernetes primitives and experimental cluster helpers (feature-gated: kubernetes)",
255 visible_alias = "k8s"
256 )]
257 Kubernetes(KubernetesCmd),
258 #[cfg(feature = "nordvpn")]
259 #[command(about = "NordVPN meshnet setup and passthrough (feature-gated)")]
260 Nordvpn(NordvpnCmd),
261 #[cfg(feature = "monitoring")]
262 Monitoring(MonitoringCmd),
263 #[command(about = "Manage the XBP API server")]
264 Api(ApiCmd),
265 #[command(about = "Manage runner hosts, groups, inventory, and runner jobs")]
266 Runners(RunnersCmd),
267 #[command(about = "Built-in MCP server for AI agents (HTTP/SSE on port 1113)")]
268 Mcp(McpCmd),
269 #[command(
270 about = "Watch git worktree mutations, store local JSONL spools, and sync them to xbp.app",
271 visible_alias = "watch"
272 )]
273 WorktreeWatch(WorktreeWatchCmd),
274 #[cfg(feature = "linear")]
275 #[command(
276 about = "Manage Linear issues. Bare `xbp linear` opens a full-screen vim-style TUI; use list/show/create/… for scripts (feature-gated: linear)"
277 )]
278 Linear(LinearIssuesCmd),
279 #[command(
280 about = "Manage GitHub issues for the current repo. Run without a subcommand for the interactive hub",
281 visible_alias = "gh"
282 )]
283 Github(GithubIssuesCmd),
284 #[command(
285 about = "Scan code markers and idempotently file Linear/GitHub issues (manual only)",
286 visible_alias = "issue"
287 )]
288 Issues(TodosCmd),
289 #[command(
290 about = "Deprecated alias for `xbp issues`: scan TODO/FIXME markers and file issues"
291 )]
292 Todos(TodosCmd),
293 #[cfg(feature = "docker")]
294 #[command(about = "Pass-through wrapper around the Docker CLI")]
295 Docker(DockerCmd),
296}
297
298pub fn command_label(command: &Commands) -> &'static str {
299 match command {
300 Commands::Ports(_) => "ports",
301 Commands::Commit(_) => "commit",
302 Commands::Push => "push",
303 Commands::Deploy(_) => "deploy",
304 Commands::Oci(_) => "oci",
305 Commands::Init => "init",
306 Commands::Setup => "setup",
307 Commands::Redeploy { .. } => "redeploy",
308 Commands::RedeployV2(_) => "redeploy-v2",
309 Commands::Config(_) => "config",
310 Commands::Install { .. } => "install",
311 Commands::Logs(_) => "logs",
312 Commands::Ssh(_) => "ssh",
313 Commands::Cloudflared(_) => "cloudflared",
314 Commands::List => "list",
315 Commands::Curl(_) => "curl",
316 Commands::Services => "services",
317 Commands::Service { .. } => "service",
318 Commands::Nginx(_) => "nginx",
319 Commands::Network(_) => "network",
320 Commands::Diag(_) => "diag",
321 Commands::Monitor(_) => "monitor",
322 Commands::Snapshot => "snapshot",
323 Commands::Resurrect => "resurrect",
324 Commands::Stop { .. } => "stop",
325 Commands::Flush { .. } => "flush",
326 Commands::Login(_) => "login",
327 Commands::Whoami => "whoami",
328 Commands::Update(_) => "update",
329 Commands::Version(_) => "version",
330 Commands::Publish(_) => "publish",
331 Commands::Env { .. } => "env",
332 Commands::Tail(_) => "tail",
333 Commands::Start { .. } => "start",
334 Commands::Generate(_) => "generate",
335 #[cfg(feature = "secrets")]
336 Commands::Secrets(_) => "secrets",
337 Commands::Workers(_) => "workers",
338 Commands::Cloudflare(_) => "cloudflare",
339 Commands::Dns(_) => "dns",
340 Commands::Domains(_) => "domains",
341 Commands::Done(_) => "done",
342 Commands::FixProcessMonitorJson(_) => "fix-process-monitor-json",
343 Commands::Cursor(_) => "cursor",
344 #[cfg(feature = "kubernetes")]
345 Commands::Kubernetes(_) => "kubernetes",
346 #[cfg(feature = "nordvpn")]
347 Commands::Nordvpn(_) => "nordvpn",
348 #[cfg(feature = "monitoring")]
349 Commands::Monitoring(_) => "monitoring",
350 Commands::Api(_) => "api",
351 Commands::Runners(_) => "runners",
352 Commands::Mcp(_) => "mcp",
353 Commands::WorktreeWatch(_) => "worktree-watch",
354 #[cfg(feature = "linear")]
355 Commands::Linear(_) => "linear",
356 Commands::Github(_) => "github",
357 Commands::Issues(_) => "issues",
358 Commands::Todos(_) => "todos",
359 #[cfg(feature = "docker")]
360 Commands::Docker(_) => "docker",
361 }
362}
363
364#[cfg(feature = "linear")]
369#[derive(Args, Debug)]
370pub struct LinearIssuesCmd {
371 #[command(subcommand)]
372 pub command: Option<LinearSubCommand>,
373 #[arg(long)]
375 pub me: bool,
376 #[arg(long, help = "Team key, name, or id")]
377 pub team: Option<String>,
378 #[arg(
379 long,
380 help = "State name or type (backlog|unstarted|started|completed|canceled)"
381 )]
382 pub state: Option<String>,
383 #[arg(long, help = "Assignee name, email, id, or `me` (overridden by --me)")]
384 pub assignee: Option<String>,
385 #[arg(long, short = 'q', help = "Filter by title/description substring")]
386 pub query: Option<String>,
387 #[arg(long = "label", help = "Require label name (repeatable; client-side)")]
388 pub labels: Vec<String>,
389 #[arg(
390 long,
391 help = "Include completed/canceled issues (default: open-ish only)"
392 )]
393 pub include_completed: bool,
394 #[arg(
395 long,
396 default_value = "updated",
397 help = "Sort: updated|created|priority|identifier|title|state"
398 )]
399 pub sort: String,
400 #[arg(long, help = "Sort ascending (default for most fields is descending)")]
401 pub asc: bool,
402 #[arg(long, default_value_t = 100, help = "Max issues to fetch for the TUI")]
403 pub limit: usize,
404}
405
406#[cfg(feature = "linear")]
408pub type LinearCmd = LinearIssuesCmd;
409
410#[cfg(feature = "linear")]
411#[derive(Subcommand, Debug)]
412pub enum LinearSubCommand {
413 #[command(about = "List issues")]
414 List(LinearListCmd),
415 #[command(about = "Search Linear issues by title/description/identifier")]
416 Search(LinearSearchCmd),
417 #[command(about = "Show a single issue")]
418 Show(LinearShowCmd),
419 #[command(about = "Create an issue")]
420 Create(LinearCreateCmd),
421 #[command(about = "Edit title/description/priority/state/labels/assignee")]
422 Edit(LinearEditCmd),
423 #[command(about = "Change issue status/workflow state")]
424 Status(LinearStatusCmd),
425 #[command(about = "Change issue priority (0–4 or none|urgent|high|medium|low)")]
426 Priority(LinearPriorityCmd),
427 #[command(about = "Set or multi-select labels")]
428 Labels(LinearLabelsCmd),
429 #[command(about = "Assign or unassign")]
430 Assign(LinearAssignCmd),
431 #[command(
432 about = "Multi-select issues and bulk assign / status / labels / project / cycle / priority"
433 )]
434 Bulk(LinearBulkCmd),
435 #[command(about = "Post a comment")]
436 Comment(LinearCommentCmd),
437 #[command(about = "List comments on an issue")]
438 Comments(LinearCommentsCmd),
439}
440
441#[cfg(feature = "linear")]
442#[derive(Args, Debug)]
443pub struct LinearListCmd {
444 #[arg(long, help = "Only issues assigned to you (viewer)")]
445 pub me: bool,
446 #[arg(long, help = "Team key, name, or id")]
447 pub team: Option<String>,
448 #[arg(
449 long,
450 help = "State name or type (backlog|unstarted|started|completed|canceled)"
451 )]
452 pub state: Option<String>,
453 #[arg(long, help = "Assignee name, email, id, or `me` (overridden by --me)")]
454 pub assignee: Option<String>,
455 #[arg(
456 long,
457 short = 'q',
458 help = "Filter by title/description substring (client-side)"
459 )]
460 pub query: Option<String>,
461 #[arg(long = "label", help = "Require label name (repeatable; client-side)")]
462 pub labels: Vec<String>,
463 #[arg(
464 long,
465 help = "Include completed/canceled issues (default: open-ish only)"
466 )]
467 pub include_completed: bool,
468 #[arg(
469 long,
470 default_value = "updated",
471 help = "Sort: updated|created|priority|identifier|title|state"
472 )]
473 pub sort: String,
474 #[arg(long, help = "Sort ascending")]
475 pub asc: bool,
476 #[arg(long, default_value_t = 50, help = "Max issues to fetch")]
477 pub limit: usize,
478 #[arg(
479 long,
480 help = "Open the full-screen Linear TUI instead of a plain table"
481 )]
482 pub tui: bool,
483}
484
485#[cfg(feature = "linear")]
486#[derive(Args, Debug)]
487pub struct LinearSearchCmd {
488 #[arg(help = "Title, description, or identifier substring")]
489 pub query: String,
490 #[arg(long, help = "Only issues assigned to you (viewer)")]
491 pub me: bool,
492 #[arg(long, help = "Team key, name, or id")]
493 pub team: Option<String>,
494 #[arg(
495 long,
496 help = "State name or type (backlog|unstarted|started|completed|canceled)"
497 )]
498 pub state: Option<String>,
499 #[arg(long, help = "Assignee name, email, id, or `me` (overridden by --me)")]
500 pub assignee: Option<String>,
501 #[arg(long = "label", help = "Require label name (repeatable; client-side)")]
502 pub labels: Vec<String>,
503 #[arg(
504 long,
505 help = "Include completed/canceled issues (default: open-ish only)"
506 )]
507 pub include_completed: bool,
508 #[arg(
509 long,
510 default_value = "updated",
511 help = "Sort: updated|created|priority|identifier|title|state"
512 )]
513 pub sort: String,
514 #[arg(long, help = "Sort ascending")]
515 pub asc: bool,
516 #[arg(long, default_value_t = 50, help = "Max issues to fetch")]
517 pub limit: usize,
518 #[arg(
519 long,
520 help = "Open the full-screen Linear TUI with the search preloaded"
521 )]
522 pub tui: bool,
523}
524
525#[cfg(feature = "linear")]
526#[derive(Args, Debug)]
527pub struct LinearShowCmd {
528 #[arg(help = "Issue id or identifier (e.g. XLX-29)")]
529 pub id: String,
530}
531
532#[cfg(feature = "linear")]
533#[derive(Args, Debug)]
534pub struct LinearCreateCmd {
535 #[arg(long, help = "Issue title")]
536 pub title: Option<String>,
537 #[arg(long, help = "Team key, name, or id")]
538 pub team: Option<String>,
539 #[arg(long, help = "Markdown description")]
540 pub description: Option<String>,
541 #[arg(long, help = "Priority 0–4 or none|urgent|high|medium|low")]
542 pub priority: Option<String>,
543 #[arg(long, help = "Initial state name or id")]
544 pub state: Option<String>,
545 #[arg(long, help = "Assignee name, email, id, or `me`")]
546 pub assignee: Option<String>,
547 #[arg(long = "label", help = "Label name or id (repeatable)")]
548 pub labels: Vec<String>,
549 #[arg(long, help = "Prompt for description when interactive")]
550 pub interactive_body: bool,
551}
552
553#[cfg(feature = "linear")]
554#[derive(Args, Debug)]
555pub struct LinearEditCmd {
556 #[arg(help = "Issue id or identifier")]
557 pub id: String,
558 #[arg(long)]
559 pub title: Option<String>,
560 #[arg(long)]
561 pub description: Option<String>,
562 #[arg(long)]
563 pub priority: Option<String>,
564 #[arg(long)]
565 pub state: Option<String>,
566 #[arg(long)]
567 pub assignee: Option<String>,
568 #[arg(long = "label")]
569 pub labels: Vec<String>,
570}
571
572#[cfg(feature = "linear")]
573#[derive(Args, Debug)]
574pub struct LinearStatusCmd {
575 #[arg(help = "Issue id or identifier")]
576 pub id: String,
577 #[arg(long, help = "State name, type, or id")]
578 pub state: Option<String>,
579}
580
581#[cfg(feature = "linear")]
582#[derive(Args, Debug)]
583pub struct LinearPriorityCmd {
584 #[arg(help = "Issue id or identifier")]
585 pub id: String,
586 #[arg(long, help = "Priority 0–4 or none|urgent|high|medium|low")]
587 pub priority: Option<String>,
588}
589
590#[cfg(feature = "linear")]
591#[derive(Args, Debug)]
592pub struct LinearLabelsCmd {
593 #[arg(help = "Issue id or identifier")]
594 pub id: String,
595 #[arg(long, help = "Label to add (repeatable)")]
596 pub add: Vec<String>,
597 #[arg(long, help = "Label to remove (repeatable)")]
598 pub remove: Vec<String>,
599 #[arg(long, help = "Replace all labels (repeatable)")]
600 pub set: Vec<String>,
601 #[arg(long, help = "Clear all labels")]
602 pub clear: bool,
603}
604
605#[cfg(feature = "linear")]
606#[derive(Args, Debug)]
607pub struct LinearAssignCmd {
608 #[arg(help = "Issue id or identifier")]
609 pub id: String,
610 #[arg(long, help = "Assignee name/email/id/`me`/`none`")]
611 pub assignee: Option<String>,
612}
613
614#[cfg(feature = "linear")]
615#[derive(Args, Debug)]
616pub struct LinearBulkCmd {
617 #[arg(long = "id", help = "Issue id or identifier (repeatable)")]
619 pub ids: Vec<String>,
620 #[arg(long, help = "Only issues assigned to you (viewer)")]
621 pub me: bool,
622 #[arg(long, help = "Team key, name, or id (candidate filter)")]
623 pub team: Option<String>,
624 #[arg(long, help = "State name or type filter for candidates")]
625 pub state: Option<String>,
626 #[arg(long, help = "Assignee filter for candidates (`me`/name/email/id)")]
627 pub assignee: Option<String>,
628 #[arg(long, short = 'q', help = "Title/description substring filter")]
629 pub query: Option<String>,
630 #[arg(long = "label", help = "Require label on candidates (repeatable)")]
631 pub filter_labels: Vec<String>,
632 #[arg(long, help = "Include completed/canceled issues in the candidate list")]
633 pub include_completed: bool,
634 #[arg(
635 long,
636 default_value = "updated",
637 help = "Sort: updated|created|priority|identifier|title|state"
638 )]
639 pub sort: String,
640 #[arg(long, help = "Sort ascending")]
641 pub asc: bool,
642 #[arg(long, default_value_t = 100, help = "Max candidate issues to fetch")]
643 pub limit: usize,
644 #[arg(long, help = "Bulk-assign to this user (`me`/`none`/name/email/id)")]
645 pub assign: Option<String>,
646 #[arg(long = "set-state", help = "Bulk set workflow state name/type/id")]
647 pub set_state: Option<String>,
648 #[arg(
649 long = "add-label",
650 help = "Label to add to all selected issues (repeatable)"
651 )]
652 pub add_labels: Vec<String>,
653 #[arg(long, help = "Project name/id (`none` to clear)")]
654 pub project: Option<String>,
655 #[arg(long, help = "Cycle name/number/id (`none` to clear; one team only)")]
656 pub cycle: Option<String>,
657 #[arg(long, help = "Priority 0–4 or none|urgent|high|medium|low")]
658 pub priority: Option<String>,
659 #[arg(long, short = 'y', help = "Skip confirmation")]
660 pub yes: bool,
661 #[arg(long, help = "Show plan only; do not write")]
662 pub dry_run: bool,
663}
664
665#[cfg(feature = "linear")]
666#[derive(Args, Debug)]
667pub struct LinearCommentCmd {
668 #[arg(help = "Issue id or identifier")]
669 pub id: String,
670 #[arg(long, short = 'm', help = "Comment body (markdown)")]
671 pub body: Option<String>,
672}
673
674#[cfg(feature = "linear")]
675#[derive(Args, Debug)]
676pub struct LinearCommentsCmd {
677 #[arg(help = "Issue id or identifier")]
678 pub id: String,
679}
680
681#[derive(Args, Debug)]
686pub struct GithubIssuesCmd {
687 #[arg(long, help = "Repository owner (default: git origin)")]
688 pub owner: Option<String>,
689 #[arg(long, help = "Repository name (default: git origin)")]
690 pub repo: Option<String>,
691 #[command(subcommand)]
692 pub command: Option<GithubSubCommand>,
693}
694
695pub type GithubCmd = GithubIssuesCmd;
697
698#[derive(Subcommand, Debug)]
699pub enum GithubSubCommand {
700 #[command(about = "List issues")]
701 List(GithubListCmd),
702 #[command(about = "Show a single issue")]
703 Show(GithubShowCmd),
704 #[command(about = "Create an issue")]
705 Create(GithubCreateCmd),
706 #[command(about = "Edit title/body/labels")]
707 Edit(GithubEditCmd),
708 #[command(about = "Open or close an issue")]
709 Status(GithubStatusCmd),
710 #[command(about = "Set labels")]
711 Labels(GithubLabelsCmd),
712 #[command(about = "Assign or unassign")]
713 Assign(GithubAssignCmd),
714 #[command(about = "Post a comment")]
715 Comment(GithubCommentCmd),
716 #[command(about = "List comments")]
717 Comments(GithubCommentsCmd),
718}
719
720#[derive(Args, Debug)]
721pub struct GithubListCmd {
722 #[arg(long, default_value = "open", help = "open | closed | all")]
723 pub state: String,
724 #[arg(long = "label", help = "Label filter (repeatable)")]
725 pub labels: Vec<String>,
726 #[arg(long, help = "Assignee login or `none`")]
727 pub assignee: Option<String>,
728 #[arg(long, short = 'q', help = "Client-side title/body filter")]
729 pub query: Option<String>,
730 #[arg(long, default_value_t = 50)]
731 pub limit: usize,
732}
733
734#[derive(Args, Debug)]
735pub struct GithubShowCmd {
736 #[arg(help = "Issue number or URL")]
737 pub id: String,
738}
739
740#[derive(Args, Debug)]
741pub struct GithubCreateCmd {
742 #[arg(long)]
743 pub title: Option<String>,
744 #[arg(long)]
745 pub body: Option<String>,
746 #[arg(long = "label")]
747 pub labels: Vec<String>,
748 #[arg(long = "assignee")]
749 pub assignees: Vec<String>,
750 #[arg(long, help = "Prompt for body when interactive")]
751 pub interactive_body: bool,
752}
753
754#[derive(Args, Debug)]
755pub struct GithubEditCmd {
756 #[arg(help = "Issue number")]
757 pub id: String,
758 #[arg(long)]
759 pub title: Option<String>,
760 #[arg(long)]
761 pub body: Option<String>,
762 #[arg(long = "label")]
763 pub labels: Vec<String>,
764}
765
766#[derive(Args, Debug)]
767pub struct GithubStatusCmd {
768 #[arg(help = "Issue number")]
769 pub id: String,
770 #[arg(long, help = "open or closed")]
771 pub state: Option<String>,
772}
773
774#[derive(Args, Debug)]
775pub struct GithubLabelsCmd {
776 #[arg(help = "Issue number")]
777 pub id: String,
778 #[arg(long)]
779 pub add: Vec<String>,
780 #[arg(long)]
781 pub remove: Vec<String>,
782 #[arg(long)]
783 pub set: Vec<String>,
784 #[arg(long)]
785 pub clear: bool,
786}
787
788#[derive(Args, Debug)]
789pub struct GithubAssignCmd {
790 #[arg(help = "Issue number")]
791 pub id: String,
792 #[arg(long, help = "GitHub login, or `none` to unassign")]
793 pub assignee: Option<String>,
794}
795
796#[derive(Args, Debug)]
797pub struct GithubCommentCmd {
798 #[arg(help = "Issue number")]
799 pub id: String,
800 #[arg(long, short = 'm')]
801 pub body: Option<String>,
802}
803
804#[derive(Args, Debug)]
805pub struct GithubCommentsCmd {
806 #[arg(help = "Issue number")]
807 pub id: String,
808}
809
810#[derive(Args, Debug)]
815pub struct TodosCmd {
816 #[command(subcommand)]
817 pub command: Option<TodosSubCommand>,
818}
819
820#[derive(Subcommand, Debug)]
821pub enum TodosSubCommand {
822 #[command(about = "Scan the codebase for TODO/FIXME markers (default)")]
823 Scan(TodosScanCmd),
824 #[command(about = "Idempotently create Linear and/or GitHub issues from code markers")]
825 Sync(TodosSyncCmd),
826 #[command(about = "Show per-marker linkage table + ledger summary")]
827 Status(TodosStatusCmd),
828 #[command(about = "Remove ledger entries for markers no longer present in code")]
829 Prune(TodosPruneCmd),
830 #[command(
831 about = "Measure coding time per linked issue from worktree-watch edits/commits on tracked paths"
832 )]
833 Effort(TodosEffortCmd),
834 #[command(
835 about = "Mark ledger issues done when Linear/GitHub report completed/closed (stops time accumulation)"
836 )]
837 Reconcile(TodosReconcileCmd),
838 #[command(about = "Search Linear and/or GitHub issues")]
839 Search(IssueSearchCmd),
840 #[command(about = "Interactive wizard: write issues automation into .xbp/xbp.toml")]
841 Setup,
842}
843
844#[derive(Args, Debug)]
845pub struct IssueSearchCmd {
846 #[arg(help = "Title/body/identifier substring")]
847 pub query: String,
848 #[arg(long, help = "Search GitHub issues")]
849 pub github: bool,
850 #[cfg(feature = "linear")]
851 #[arg(long, help = "Search Linear issues")]
852 pub linear: bool,
853 #[arg(long, help = "GitHub owner override")]
854 pub owner: Option<String>,
855 #[arg(long, help = "GitHub repo override")]
856 pub repo: Option<String>,
857 #[arg(long, help = "State filter")]
858 pub state: Option<String>,
859 #[arg(long = "label", help = "Require label name (repeatable; client-side)")]
860 pub labels: Vec<String>,
861 #[arg(long, help = "Assignee filter (`me` supported for Linear)")]
862 pub assignee: Option<String>,
863 #[arg(long, help = "Include completed/canceled/closed issues")]
864 pub include_completed: bool,
865 #[arg(
866 long,
867 default_value = "updated",
868 help = "Linear sort: updated|created|priority|identifier|title|state"
869 )]
870 pub sort: String,
871 #[arg(long, help = "Sort ascending")]
872 pub asc: bool,
873 #[arg(long, default_value_t = 50, help = "Max issues per provider")]
874 pub limit: usize,
875 #[cfg(feature = "linear")]
876 #[arg(long, help = "Open Linear results in the Linear TUI")]
877 pub tui: bool,
878}
879
880#[derive(Args, Debug)]
881pub struct TodosScanCmd {
882 #[arg(long, help = "Root path to scan (default: project/git root)")]
883 pub path: Option<PathBuf>,
884 #[arg(long, help = "Emit JSON")]
885 pub json: bool,
886 #[arg(
887 long,
888 help = "Do not offer interactive sync after scan (also set issues.prompt_sync_after_scan: false)"
889 )]
890 pub no_prompt: bool,
891}
892
893#[derive(Args, Debug)]
894pub struct TodosSyncCmd {
895 #[arg(
896 long,
897 value_enum,
898 help = "Where to create issues (default: issues.default_to from config, else both)"
899 )]
900 pub to: Option<TodosSyncTarget>,
901 #[arg(long, help = "Root path to scan")]
902 pub path: Option<PathBuf>,
903 #[arg(long, help = "Preview creates without writing")]
904 pub dry_run: bool,
905 #[arg(
906 long,
907 short = 'y',
908 help = "Skip interactive multi-select; file all new markers (or set issues.auto_yes: true)"
909 )]
910 pub yes: bool,
911 #[cfg(feature = "linear")]
912 #[arg(long, help = "Linear team key/name/id (requires --features linear)")]
913 pub team: Option<String>,
914 #[arg(long, help = "GitHub owner override")]
915 pub owner: Option<String>,
916 #[arg(long, help = "GitHub repo override")]
917 pub repo: Option<String>,
918 #[arg(
919 long,
920 help = "Stamp source marker lines with created issue IDs (or set issues.annotate_source: true)"
921 )]
922 pub annotate: bool,
923 #[arg(
924 long,
925 help = "Do not stamp source lines even if config enables annotate_source"
926 )]
927 pub no_annotate: bool,
928 #[arg(
929 long,
930 help = "Opt-in: enrich issue title/body with OpenRouter (overrides issues.openrouter_enrich: false)"
931 )]
932 pub enrich: bool,
933 #[arg(
934 long,
935 help = "Disable OpenRouter enrichment even if todos.openrouter_enrich is true"
936 )]
937 pub no_enrich: bool,
938}
939
940#[derive(Args, Debug)]
941pub struct TodosStatusCmd {
942 #[arg(long)]
943 pub path: Option<PathBuf>,
944}
945
946#[derive(Args, Debug)]
947pub struct TodosPruneCmd {
948 #[arg(long)]
949 pub path: Option<PathBuf>,
950 #[arg(long, help = "List stale entries without removing them")]
951 pub dry_run: bool,
952}
953
954#[derive(Args, Debug)]
955pub struct TodosEffortCmd {
956 #[arg(long, help = "Project root (default: .xbp / git root)")]
957 pub path: Option<PathBuf>,
958 #[arg(
959 long,
960 default_value_t = 15,
961 help = "Max minutes between file mutations counted as one coding session (worktree-watch gap)"
962 )]
963 pub gap_minutes: u64,
964 #[arg(long, help = "Emit JSON report")]
965 pub json: bool,
966 #[arg(long, help = "Do not write ledger/effort snapshot")]
967 pub no_persist: bool,
968}
969
970#[derive(Args, Debug)]
971pub struct TodosReconcileCmd {
972 #[arg(long, help = "Project root (default: .xbp / git root)")]
973 pub path: Option<PathBuf>,
974 #[arg(long, help = "Detect closed issues without writing the ledger")]
975 pub dry_run: bool,
976}
977
978#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
979pub enum TodosSyncTarget {
980 #[cfg(feature = "linear")]
981 Linear,
982 Github,
983 #[cfg(feature = "linear")]
984 Both,
985}
986
987#[derive(Args, Debug)]
988#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
989pub struct DeployCmd {
990 #[arg(
991 help = "Service name, deploy.groups name, or `all` (optional in a TTY — interactive picker)"
992 )]
993 pub target: Option<String>,
994 #[arg(long, help = "Deploy environment (default: deploy.default_env or production; interactive picker when omitted in a TTY)")]
995 pub env: Option<String>,
996 #[arg(long, help = "Print plan only (no mutations). Default when no other mode is set.")]
997 pub plan: bool,
998 #[arg(long, help = "Apply deploy providers for the resolved services")]
999 pub run: bool,
1000 #[arg(long, help = "Read-only verification (rollout/health/image checks)")]
1001 pub verify: bool,
1002 #[arg(long, help = "Status snapshot for resolved services")]
1003 pub status: bool,
1004 #[arg(
1005 long,
1006 value_name = "TAG_OR_ENV",
1007 help = "Promote OCI images by digest to this tag (no rebuild). Use with --run to also apply."
1008 )]
1009 pub promote: Option<String>,
1010 #[arg(long, help = "List recent deploy history records")]
1011 pub history: bool,
1012 #[arg(long, help = "Show what would happen without registry/cluster mutations")]
1013 pub dry_run: bool,
1014 #[arg(long, help = "Skip OCI existence/digest checks")]
1015 pub skip_oci: bool,
1016 #[arg(long, help = "Skip kubectl apply (still plan/verify other steps)")]
1017 pub skip_apply: bool,
1018 #[arg(long, help = "Skip workload rollout waits")]
1019 pub skip_rollout: bool,
1020 #[arg(long, help = "Skip HTTP health probes")]
1021 pub skip_health: bool,
1022 #[arg(long, help = "Override Kubernetes namespace for this deploy")]
1023 pub namespace: Option<String>,
1024 #[arg(long, help = "Override Kubernetes context for this deploy")]
1025 pub context: Option<String>,
1026 #[arg(long, help = "Skip kubernetes context confirmation prompts")]
1027 pub yes: bool,
1028 #[arg(long, help = "Emit JSON for plan/report automation")]
1029 pub json: bool,
1030 #[arg(long, value_name = "PATH", help = "Write plan/report JSON to a file")]
1031 pub output: Option<PathBuf>,
1032 #[arg(long, default_value_t = 20, help = "History entries to show with --history")]
1033 pub limit: usize,
1034}
1035
1036#[derive(Args, Debug)]
1037#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
1038pub struct OciCmd {
1039 #[command(subcommand)]
1040 pub command: OciSubCommand,
1041}
1042
1043#[derive(Subcommand, Debug)]
1044pub enum OciSubCommand {
1045 #[command(about = "Inspect an OCI image (digest, media type, layers)")]
1046 Inspect(OciInspectCmd),
1047 #[command(about = "List tags for a repository")]
1048 Tags(OciTagsCmd),
1049 #[command(about = "Resolve tag to immutable digest")]
1050 Digest(OciDigestCmd),
1051 #[command(about = "Exit 0 if image/tag/digest exists")]
1052 Exists(OciExistsCmd),
1053 #[command(about = "Promote/copy image by digest to a new tag (no rebuild)")]
1054 Promote(OciPromoteCmd),
1055 #[command(about = "Print image manifest")]
1056 PullManifest(OciPullManifestCmd),
1057 #[command(about = "List OCI referrers where supported")]
1058 Referrers(OciReferrersCmd),
1059}
1060
1061#[derive(Args, Debug)]
1062pub struct OciInspectCmd {
1063 #[arg(help = "Image reference (registry/repo:tag or @digest)")]
1064 pub image: String,
1065 #[arg(long, help = "JSON output")]
1066 pub json: bool,
1067 #[arg(long, help = "Platform filter (os/arch), e.g. linux/amd64")]
1068 pub platform: Option<String>,
1069 #[arg(long, help = "Auth provider hint (github, anonymous, …)")]
1070 pub auth: Option<String>,
1071 #[arg(long, help = "Registry username")]
1072 pub username: Option<String>,
1073 #[arg(long, help = "Registry token/password")]
1074 pub token: Option<String>,
1075 #[arg(long, help = "Force anonymous pull")]
1076 pub anonymous: bool,
1077}
1078
1079#[derive(Args, Debug)]
1080pub struct OciTagsCmd {
1081 #[arg(help = "Image or repository reference")]
1082 pub image: String,
1083 #[arg(long, help = "Max tags to return")]
1084 pub limit: Option<usize>,
1085 #[arg(long, help = "JSON output")]
1086 pub json: bool,
1087 #[arg(long, help = "Auth provider hint")]
1088 pub auth: Option<String>,
1089 #[arg(long, help = "Registry username")]
1090 pub username: Option<String>,
1091 #[arg(long, help = "Registry token/password")]
1092 pub token: Option<String>,
1093 #[arg(long, help = "Force anonymous pull")]
1094 pub anonymous: bool,
1095}
1096
1097#[derive(Args, Debug)]
1098pub struct OciDigestCmd {
1099 #[arg(help = "Image reference")]
1100 pub image: String,
1101 #[arg(long, help = "JSON output")]
1102 pub json: bool,
1103 #[arg(long, value_name = "PATH", help = "Merge digest into a deploy-lock JSON file")]
1104 pub write_lock: Option<PathBuf>,
1105 #[arg(long, help = "Auth provider hint")]
1106 pub auth: Option<String>,
1107 #[arg(long, help = "Registry username")]
1108 pub username: Option<String>,
1109 #[arg(long, help = "Registry token/password")]
1110 pub token: Option<String>,
1111 #[arg(long, help = "Force anonymous pull")]
1112 pub anonymous: bool,
1113}
1114
1115#[derive(Args, Debug)]
1116pub struct OciExistsCmd {
1117 #[arg(help = "Image reference")]
1118 pub image: String,
1119 #[arg(long, help = "JSON output")]
1120 pub json: bool,
1121 #[arg(long, help = "Auth provider hint")]
1122 pub auth: Option<String>,
1123 #[arg(long, help = "Registry username")]
1124 pub username: Option<String>,
1125 #[arg(long, help = "Registry token/password")]
1126 pub token: Option<String>,
1127 #[arg(long, help = "Force anonymous pull")]
1128 pub anonymous: bool,
1129}
1130
1131#[derive(Args, Debug)]
1132pub struct OciPromoteCmd {
1133 #[arg(help = "Source image reference")]
1134 pub source_image: String,
1135 #[arg(long = "to", value_name = "TARGET_TAG", help = "Target tag to promote to")]
1136 pub to: String,
1137 #[arg(long, help = "Print intended promote without mutating registry")]
1138 pub dry_run: bool,
1139 #[arg(long, help = "JSON output")]
1140 pub json: bool,
1141 #[arg(long, help = "Auth provider hint")]
1142 pub auth: Option<String>,
1143 #[arg(long, help = "Registry username")]
1144 pub username: Option<String>,
1145 #[arg(long, help = "Registry token/password")]
1146 pub token: Option<String>,
1147 #[arg(long, help = "Overwrite existing target tag")]
1148 pub force: bool,
1149}
1150
1151#[derive(Args, Debug)]
1152pub struct OciPullManifestCmd {
1153 #[arg(help = "Image reference")]
1154 pub image: String,
1155 #[arg(long, help = "Print raw registry response body")]
1156 pub raw: bool,
1157 #[arg(long, help = "JSON output")]
1158 pub json: bool,
1159 #[arg(long, help = "Auth provider hint")]
1160 pub auth: Option<String>,
1161 #[arg(long, help = "Registry username")]
1162 pub username: Option<String>,
1163 #[arg(long, help = "Registry token/password")]
1164 pub token: Option<String>,
1165 #[arg(long, help = "Force anonymous pull")]
1166 pub anonymous: bool,
1167}
1168
1169#[derive(Args, Debug)]
1170pub struct OciReferrersCmd {
1171 #[arg(help = "Image or digest reference")]
1172 pub image: String,
1173 #[arg(long, help = "Filter by artifact type")]
1174 pub artifact_type: Option<String>,
1175 #[arg(long, help = "JSON output")]
1176 pub json: bool,
1177 #[arg(long, help = "Auth provider hint")]
1178 pub auth: Option<String>,
1179 #[arg(long, help = "Registry username")]
1180 pub username: Option<String>,
1181 #[arg(long, help = "Registry token/password")]
1182 pub token: Option<String>,
1183 #[arg(long, help = "Force anonymous pull")]
1184 pub anonymous: bool,
1185}
1186
1187#[derive(Args, Debug)]
1188#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
1189pub struct OciConfigCmd {
1190 #[command(subcommand)]
1191 pub action: Option<OciConfigAction>,
1192}
1193
1194#[derive(Subcommand, Debug)]
1195pub enum OciConfigAction {
1196 #[command(about = "Show configured OCI registry credentials (tokens masked)")]
1197 Show,
1198 #[command(about = "Show OCI registry auth status")]
1199 Status,
1200 #[command(about = "Store a registry username/token in global config")]
1201 SetRegistryToken {
1202 #[arg(long, help = "Registry host (e.g. ghcr.io)")]
1203 registry: String,
1204 #[arg(long, help = "Registry username")]
1205 username: String,
1206 #[arg(help = "Token value (omit to enter securely)")]
1207 token: Option<String>,
1208 },
1209 #[command(about = "Delete stored registry credentials")]
1210 DeleteRegistryToken {
1211 #[arg(long, help = "Registry host (e.g. ghcr.io)")]
1212 registry: String,
1213 },
1214}
1215
1216#[derive(Args, Debug)]
1217#[command(
1218 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1219 after_help = crate::cli::help_render::COMMIT_AFTER_HELP
1220)]
1221pub struct CommitCmd {
1222 #[arg(
1223 long,
1224 help = "Generate and print the conventional commit message without creating a git commit"
1225 )]
1226 pub dry_run: bool,
1227 #[arg(
1228 short = 'p',
1229 long,
1230 help = "Push after committing, or push pending local commits when nothing new needs committing"
1231 )]
1232 pub push: bool,
1233 #[arg(long, help = "Skip OpenRouter and use local heuristics only")]
1234 pub no_ai: bool,
1235 #[arg(
1236 long,
1237 help = "OpenRouter model override used for commit generation; otherwise XBP uses the global config default"
1238 )]
1239 pub model: Option<String>,
1240 #[arg(
1241 long,
1242 help = "Force the conventional commit scope (for example: cli, api, docs)"
1243 )]
1244 pub scope: Option<String>,
1245}
1246
1247#[derive(Args, Debug)]
1248#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
1249pub struct PortsCmd {
1250 #[arg(short = 'p', long = "port")]
1251 pub port: Option<u16>,
1252 #[arg(
1253 long = "sort",
1254 default_value = "port",
1255 value_parser = ["port", "pid"],
1256 help = "Sort port rows by port or pid (default: port)"
1257 )]
1258 pub sort: String,
1259 #[arg(long = "kill")]
1260 pub kill: bool,
1261 #[arg(short = 'n', long = "nginx")]
1262 pub nginx: bool,
1263 #[arg(
1264 long = "full",
1265 help = "Show one unified ports view (reconciled listeners + exposure + security flags)"
1266 )]
1267 pub full: bool,
1268 #[arg(
1269 long = "no-local",
1270 help = "Exclude connections where LocalAddr equals RemoteAddr"
1271 )]
1272 pub no_local: bool,
1273 #[arg(
1274 long = "exposure",
1275 help = "Diagnose external exposure per port (binding + firewall layer)"
1276 )]
1277 pub exposure: bool,
1278}
1279
1280#[derive(Args, Debug)]
1281#[command(
1282 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1283 after_help = crate::cli::help_render::CONFIG_AFTER_HELP
1284)]
1285pub struct ConfigCmd {
1286 #[arg(
1287 long,
1288 help = "Show the current project config instead of opening global XBP paths"
1289 )]
1290 pub project: bool,
1291 #[arg(long, help = "Print global XBP paths without opening them")]
1292 pub no_open: bool,
1293 #[command(subcommand)]
1294 pub provider: Option<ConfigProviderCmd>,
1295}
1296
1297#[derive(ValueEnum, Clone, Debug)]
1298pub enum ConfigFileFormat {
1299 Json,
1300 Jsonc,
1301 Toml,
1302 Yaml,
1303}
1304
1305#[derive(Args, Debug)]
1306pub struct MigrateConfigFileCmd {
1307 #[arg(value_enum, help = "Destination format")]
1308 pub format: ConfigFileFormat,
1309 #[arg(
1310 long,
1311 help = "Source XBP config path; defaults to the repository-bound config"
1312 )]
1313 pub from: Option<PathBuf>,
1314 #[arg(long, help = "Destination path; defaults to .xbp/xbp.<format>")]
1315 pub output: Option<PathBuf>,
1316}
1317
1318#[derive(Subcommand, Debug)]
1319pub enum ConfigProviderCmd {
1320 #[command(
1321 name = "migrate-config-file",
1322 about = "Convert the repository-bound XBP config to JSON, JSONC, TOML, or YAML"
1323 )]
1324 MigrateConfigFile(MigrateConfigFileCmd),
1325 #[command(about = "Manage the OpenRouter API key used by AI-enabled commands")]
1326 Openrouter(ConfigSecretCmd),
1327 #[command(about = "Manage the GitHub OAuth2 token used for release automation")]
1328 Github(ConfigSecretCmd),
1329 #[command(
1330 about = "Manage Cloudflare API credentials used by secrets, DNS, and domains (run without a subcommand for the interactive setup wizard)"
1331 )]
1332 Cloudflare(CloudflareConfigCmd),
1333 #[cfg(feature = "linear")]
1334 #[command(
1335 about = "Manage the Linear API key used for release-note issue linking and initiative publishing (feature-gated: linear)"
1336 )]
1337 Linear(LinearConfigCmd),
1338 #[command(about = "Manage npm registry auth and guided npm publish config")]
1339 Npm(RegistryConfigCmd),
1340 #[command(about = "Manage crates.io auth and guided crate publish config")]
1341 Crates(CratesConfigCmd),
1342 #[command(about = "Manage guided release config in .xbp/xbp.toml")]
1343 Release(ReleaseConfigCmd),
1344 #[command(
1345 about = "Manage Discord webhook notifications for releases, registry publishes, cargo-dist, Linear posts, and OCI deploys (run without a subcommand for the setup wizard)"
1346 )]
1347 Discord(DiscordConfigCmd),
1348 #[command(
1349 about = "Interactively manage static OpenAPI generation in .xbp/xbp.toml (run without a subcommand for the setup wizard)"
1350 )]
1351 Openapi(OpenapiConfigCmd),
1352 #[command(
1353 about = "Manage OCI registry credentials in global config (tokens masked by default)"
1354 )]
1355 Oci(OciConfigCmd),
1356}
1357
1358#[derive(Args, Debug)]
1359pub struct ConfigSecretCmd {
1360 #[command(subcommand)]
1361 pub action: ConfigSecretAction,
1362}
1363
1364#[derive(Subcommand, Debug)]
1365pub enum ConfigSecretAction {
1366 #[command(about = "Set provider key (omit value to enter it securely)")]
1367 SetKey {
1368 #[arg(help = "Provider key/token value")]
1369 key: Option<String>,
1370 },
1371 #[command(about = "Delete the stored provider key")]
1372 DeleteKey,
1373 #[command(about = "Show whether a key is configured (masked by default)")]
1374 Show {
1375 #[arg(long, help = "Print full key/token value (not masked)")]
1376 raw: bool,
1377 },
1378}
1379
1380#[derive(Args, Debug)]
1381pub struct CloudflareConfigCmd {
1382 #[command(subcommand)]
1383 pub action: Option<CloudflareConfigAction>,
1384}
1385
1386#[derive(Subcommand, Debug)]
1387pub enum CloudflareConfigAction {
1388 #[command(about = "Set Cloudflare API token (omit value to enter it securely)")]
1389 SetKey {
1390 #[arg(help = "Cloudflare API token")]
1391 key: Option<String>,
1392 },
1393 #[command(about = "Delete the stored Cloudflare API token")]
1394 DeleteKey,
1395 #[command(about = "Show whether a Cloudflare API token is configured")]
1396 ShowKey {
1397 #[arg(long, help = "Print full token value (not masked)")]
1398 raw: bool,
1399 },
1400 #[command(about = "Set the default Cloudflare account ID")]
1401 SetAccountId {
1402 #[arg(help = "Cloudflare account ID")]
1403 account_id: Option<String>,
1404 },
1405 #[command(about = "Delete the stored default Cloudflare account ID")]
1406 DeleteAccountId,
1407 #[command(about = "Show whether a Cloudflare account ID is configured")]
1408 ShowAccountId {
1409 #[arg(long, help = "Print full account ID value (not masked)")]
1410 raw: bool,
1411 },
1412 #[command(about = "Interactive dashboard OAuth linking flow for Cloudflare credentials")]
1413 Login,
1414 #[command(about = "Show Cloudflare credential sources and readiness")]
1415 Status,
1416 #[command(about = "Run the interactive Cloudflare credential setup wizard")]
1417 Setup,
1418}
1419
1420#[cfg(feature = "linear")]
1421#[derive(Args, Debug)]
1422pub struct LinearConfigCmd {
1423 #[command(subcommand)]
1424 pub action: LinearConfigAction,
1425}
1426
1427#[cfg(feature = "linear")]
1428#[derive(Subcommand, Debug)]
1429pub enum LinearConfigAction {
1430 #[command(about = "Set Linear API key (omit value to enter it securely)")]
1431 SetKey {
1432 #[arg(help = "Linear API key/token value")]
1433 key: Option<String>,
1434 },
1435 #[command(about = "Delete the stored Linear API key")]
1436 DeleteKey,
1437 #[command(about = "Show whether a Linear API key is configured (masked by default)")]
1438 Show {
1439 #[arg(long, help = "Print full key/token value (not masked)")]
1440 raw: bool,
1441 },
1442 #[command(
1443 name = "select-initiative",
1444 about = "Pick a Linear initiative for the current repo and save it to .xbp/xbp.toml"
1445 )]
1446 SelectInitiative,
1447}
1448
1449#[derive(Args, Debug)]
1450pub struct RegistryConfigCmd {
1451 #[command(subcommand)]
1452 pub action: RegistryConfigAction,
1453}
1454
1455#[derive(Args, Debug)]
1456pub struct CratesConfigCmd {
1457 #[command(subcommand)]
1458 pub action: CratesConfigAction,
1459}
1460
1461#[derive(Args, Debug)]
1462pub struct ReleaseConfigCmd {
1463 #[command(subcommand)]
1464 pub action: ReleaseConfigAction,
1465}
1466
1467#[derive(Args, Debug)]
1468pub struct DiscordConfigCmd {
1469 #[command(subcommand)]
1470 pub action: Option<DiscordConfigAction>,
1471}
1472
1473#[derive(Subcommand, Debug)]
1474pub enum DiscordConfigAction {
1475 #[command(about = "Interactive Discord webhook + per-event setup (project and/or global)")]
1476 Setup,
1477 #[command(about = "Set Discord webhook URL (omit value to enter it securely)")]
1478 SetUrl {
1479 #[arg(help = "Discord webhook URL or ${DISCORD_WEBHOOK_URL} placeholder")]
1480 url: Option<String>,
1481 #[arg(
1482 long,
1483 help = "Write to global ~/.xbp/config.yaml instead of the project .xbp/xbp.toml"
1484 )]
1485 global: bool,
1486 },
1487 #[command(about = "Delete the configured Discord webhook URL")]
1488 DeleteUrl {
1489 #[arg(long, help = "Remove from global config instead of project config")]
1490 global: bool,
1491 },
1492 #[command(about = "Show Discord webhook and per-event notification settings")]
1493 Show {
1494 #[arg(long, help = "Print full webhook URL (not masked)")]
1495 raw: bool,
1496 },
1497 #[command(about = "Interactively enable/disable individual notification events")]
1498 Events {
1499 #[arg(long, help = "Edit global event defaults instead of project config")]
1500 global: bool,
1501 },
1502 #[command(
1503 about = "Interactively enable/disable Discord OCI (and other) events per service in .xbp/xbp.toml"
1504 )]
1505 Services,
1506 #[command(about = "Send a test Discord webhook embed using the resolved config")]
1507 Test,
1508}
1509
1510#[derive(Args, Debug)]
1511pub struct OpenapiConfigCmd {
1512 #[command(subcommand)]
1513 pub action: Option<OpenapiConfigAction>,
1514}
1515
1516#[derive(Subcommand, Debug)]
1517pub enum OpenapiConfigAction {
1518 #[command(about = "Run the interactive OpenAPI setup wizard for .xbp/xbp.toml")]
1519 Setup,
1520 #[command(about = "Show project and per-service OpenAPI generation settings")]
1521 Show,
1522}
1523
1524#[derive(Subcommand, Debug)]
1525pub enum RegistryConfigAction {
1526 #[command(about = "Set registry token/key (omit value to enter it securely)")]
1527 SetKey {
1528 #[arg(help = "Registry token value")]
1529 key: Option<String>,
1530 },
1531 #[command(about = "Delete the stored registry token")]
1532 DeleteKey,
1533 #[command(about = "Show whether a registry token is configured (masked by default)")]
1534 Show {
1535 #[arg(long, help = "Print full token value (not masked)")]
1536 raw: bool,
1537 },
1538 #[command(
1539 name = "setup-release",
1540 about = "Interactively configure project publish settings in .xbp/xbp.toml"
1541 )]
1542 SetupRelease,
1543}
1544
1545#[derive(Subcommand, Debug)]
1546pub enum CratesConfigAction {
1547 #[command(about = "Set crates.io token (omit value to enter it securely)")]
1548 SetKey {
1549 #[arg(help = "crates.io token value")]
1550 key: Option<String>,
1551 },
1552 #[command(about = "Delete the stored crates.io token from global XBP config")]
1553 DeleteKey,
1554 #[command(about = "Show whether a crates.io token is configured (masked by default)")]
1555 Show {
1556 #[arg(long, help = "Print full token value (not masked)")]
1557 raw: bool,
1558 },
1559 #[command(
1560 name = "setup-release",
1561 about = "Interactively configure project publish settings in .xbp/xbp.toml"
1562 )]
1563 SetupRelease,
1564 #[command(
1565 about = "Run `cargo login` using the stored crates token and sync Cargo's local credentials file"
1566 )]
1567 Login {
1568 #[arg(help = "Optional crates.io token value to save before logging in")]
1569 key: Option<String>,
1570 },
1571 #[command(
1572 about = "Run `cargo logout` to remove Cargo's local crates.io credentials while keeping XBP's stored token"
1573 )]
1574 Logout,
1575}
1576
1577#[derive(Subcommand, Debug)]
1578pub enum ReleaseConfigAction {
1579 #[command(
1580 name = "setup",
1581 about = "Interactively configure release tag naming in .xbp/xbp.toml"
1582 )]
1583 Setup,
1584}
1585
1586#[derive(Args, Debug)]
1587#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
1588pub struct CurlCmd {
1589 #[arg(help = "URL or domain to fetch, e.g. example.com or https://example.com/api")]
1590 pub url: Option<String>,
1591 #[arg(long, help = "Disable the default 15 second timeout")]
1592 pub no_timeout: bool,
1593}
1594
1595#[derive(Args, Debug)]
1596#[command(
1597 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1598 after_help = crate::cli::help_render::LOGIN_AFTER_HELP
1599)]
1600pub struct LoginCmd {
1601 #[command(subcommand)]
1602 pub action: Option<LoginSubCommand>,
1603}
1604
1605#[derive(Subcommand, Debug)]
1606pub enum LoginSubCommand {
1607 #[command(about = "Show whether the current CLI session is still valid")]
1608 Status,
1609 #[command(about = "Revoke the current CLI token and clear local login state")]
1610 Logout,
1611}
1612
1613#[derive(Args, Debug)]
1614#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
1615pub struct UpdateCmd {
1616 #[arg(
1617 long = "crate",
1618 default_value = "xbp",
1619 help = "crates.io package name to check (default: xbp)"
1620 )]
1621 pub crate_name: String,
1622 #[arg(long, help = "Print the update check result as JSON")]
1623 pub json: bool,
1624 #[arg(
1625 long,
1626 help = "Run `cargo install <crate> --locked --force` when a newer release is available (defaults to --features all)"
1627 )]
1628 pub install: bool,
1629 #[arg(
1630 long = "features",
1631 value_name = "FEATURES",
1632 value_delimiter = ',',
1633 num_args = 1..,
1634 help = "Comma-separated Cargo features for `cargo install` (repeatable). When --install is set and this is omitted, defaults to `all`"
1635 )]
1636 pub features: Vec<String>,
1637 #[arg(
1638 long = "no-default-features",
1639 help = "Pass --no-default-features to `cargo install` (only with --install)"
1640 )]
1641 pub no_default_features: bool,
1642 #[arg(
1643 long = "all-features",
1644 help = "Pass --all-features to `cargo install` (only with --install; overrides --features)"
1645 )]
1646 pub all_features: bool,
1647 #[arg(
1648 long,
1649 help = "Exit with an error when the installed CLI is older than crates.io"
1650 )]
1651 pub fail_if_outdated: bool,
1652}
1653
1654#[derive(Args, Debug)]
1655#[command(
1656 subcommand_precedence_over_arg = true,
1657 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1658 after_help = crate::cli::help_render::VERSION_AFTER_HELP
1659)]
1660pub struct VersionCmd {
1661 #[arg(
1662 short = 'p',
1663 long,
1664 help = "Push after auto-committing version file updates"
1665 )]
1666 pub push: bool,
1667 #[arg(
1668 help = "Show versions, bump with major/minor/patch, or set an explicit version like 1.2.3"
1669 )]
1670 pub target: Option<String>,
1671 #[arg(
1672 short = 'v',
1673 long = "version",
1674 help = "Explicit version target; equivalent to the positional version value and overrides it when both are provided"
1675 )]
1676 pub explicit_version: Option<String>,
1677 #[arg(long, help = "Show normalized git tags from `git tag --list`")]
1678 pub git: bool,
1679 #[command(subcommand)]
1680 pub command: Option<VersionSubCommand>,
1681}
1682
1683#[derive(Subcommand, Debug)]
1684pub enum VersionSubCommand {
1685 #[command(
1686 about = "Create and push a git tag for this version, then create a GitHub release",
1687 visible_alias = "r"
1688 )]
1689 Release(VersionReleaseCmd),
1690 #[command(
1691 about = "Manage Rust workspace release/version drift, sync, validation, and publish flow",
1692 arg_required_else_help = true
1693 )]
1694 Workspace(VersionWorkspaceCmd),
1695 #[command(
1696 about = "Manage explicit release-domain ownership, validation, sync, diagnosis, and guarded releases",
1697 arg_required_else_help = true
1698 )]
1699 Domain(VersionDomainCmd),
1700 #[command(name = "discover", alias = "register", alias = "register-services")]
1702 Discover(VersionDiscoverServicesCmd),
1703 #[command(
1704 about = "Bump package versions from dirty worktree paths or commits since last release",
1705 visible_alias = "b"
1706 )]
1707 Bump(VersionBumpCmd),
1708}
1709
1710#[derive(Args, Debug)]
1711pub struct VersionBumpCmd {
1712 #[arg(
1713 short = 'p',
1714 long,
1715 help = "Push after auto-committing bumped version files"
1716 )]
1717 pub push: bool,
1718 #[arg(long, help = "Preview bump selections without writing version files")]
1719 pub dry_run: bool,
1720 #[arg(
1721 long,
1722 help = "Print a rich plan (baselines, files, suggested kinds) without writing; implies dry-run"
1723 )]
1724 pub plan: bool,
1725 #[arg(
1726 long,
1727 help = "Non-interactive: apply suggested bump kinds for packages with changes"
1728 )]
1729 pub auto: bool,
1730 #[arg(
1731 long,
1732 help = "On a clean tree, also offer scopes with no commits since the last release"
1733 )]
1734 pub include_unchanged: bool,
1735 #[arg(
1736 long,
1737 help = "After bumps succeed, run `version release` for each bumped scope (skips release picker when combined with --auto)"
1738 )]
1739 pub release: bool,
1740 #[arg(
1741 long,
1742 help = "Do not prompt to continue with version release after bumps"
1743 )]
1744 pub no_release_prompt: bool,
1745 #[arg(
1746 long,
1747 group = "bump_kind",
1748 help = "Default bump kind for --all and interactive default selection"
1749 )]
1750 pub major: bool,
1751 #[arg(
1752 long,
1753 group = "bump_kind",
1754 help = "Default bump kind for --all and interactive default"
1755 )]
1756 pub minor: bool,
1757 #[arg(
1758 long,
1759 group = "bump_kind",
1760 help = "Default bump kind for --all and interactive default"
1761 )]
1762 pub patch: bool,
1763 #[arg(
1764 long,
1765 help = "Bump every candidate package with the selected default kind without per-package prompts"
1766 )]
1767 pub all: bool,
1768}
1769
1770#[derive(Args, Debug)]
1771pub struct VersionDiscoverServicesCmd {
1772 #[arg(
1773 long,
1774 help = "Preview discovered nested XBP services without writing .xbp/xbp.toml"
1775 )]
1776 pub dry_run: bool,
1777 #[arg(
1778 long,
1779 help = "Discover nested services only; do not register them in the root config (opt out)"
1780 )]
1781 pub no_register: bool,
1782}
1783
1784#[derive(Args, Debug)]
1785pub struct VersionReleaseCmd {
1786 #[arg(
1787 long,
1788 help = "Release this version instead of auto-detecting from tracked files"
1789 )]
1790 pub version: Option<String>,
1791 #[arg(
1792 long = "flag",
1793 value_enum,
1794 help = "Append build metadata to the release version: dev, stable, beta, alpha, nightly, or exp"
1795 )]
1796 pub flag: Option<VersionReleaseFlag>,
1797 #[arg(
1798 long,
1799 help = "Allow releasing with uncommitted changes in the working tree"
1800 )]
1801 pub allow_dirty: bool,
1802 #[arg(
1803 long,
1804 help = "Release title (defaults to <version>[-<flag>]-<service>)"
1805 )]
1806 pub title: Option<String>,
1807 #[arg(long, help = "Release notes body (Markdown)")]
1808 pub notes: Option<String>,
1809 #[arg(long, help = "Read release notes body from a file")]
1810 pub notes_file: Option<PathBuf>,
1811 #[arg(long, help = "Create as draft release")]
1812 pub draft: bool,
1813 #[arg(long, help = "Mark release as pre-release")]
1814 pub prerelease: bool,
1815 #[arg(
1816 long,
1817 help = "Run configured npm/crates publish workflows before creating the GitHub release"
1818 )]
1819 pub publish: bool,
1820 #[arg(
1821 long,
1822 requires = "publish",
1823 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)."
1824 )]
1825 pub force: bool,
1826 #[arg(
1827 long,
1828 help = "Plan the release (and optional package publish) without writing versions, tags, ledgers, or publishing"
1829 )]
1830 pub dry_run: bool,
1831 #[arg(
1832 long,
1833 value_enum,
1834 default_value_t = VersionReleaseLatest::Legacy,
1835 help = "Control GitHub latest flag: true, false, or legacy"
1836 )]
1837 pub make_latest: VersionReleaseLatest,
1838}
1839
1840#[derive(Copy, Clone, Debug, ValueEnum)]
1841pub enum VersionReleaseLatest {
1842 True,
1843 False,
1844 Legacy,
1845}
1846
1847#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq)]
1848pub enum VersionReleaseFlag {
1849 Dev,
1850 Stable,
1851 Beta,
1852 Alpha,
1853 Nightly,
1854 Exp,
1855}
1856
1857impl VersionReleaseFlag {
1858 pub fn as_str(self) -> &'static str {
1859 match self {
1860 Self::Dev => "dev",
1861 Self::Stable => "stable",
1862 Self::Beta => "beta",
1863 Self::Alpha => "alpha",
1864 Self::Nightly => "nightly",
1865 Self::Exp => "exp",
1866 }
1867 }
1868}
1869
1870#[derive(Args, Debug)]
1871#[command(
1872 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1873 after_help = crate::cli::help_render::PUBLISH_AFTER_HELP
1874)]
1875pub struct PublishCmd {
1876 #[arg(
1877 long,
1878 help = "Validate and print what would publish without uploading packages"
1879 )]
1880 pub dry_run: bool,
1881 #[arg(
1882 long,
1883 help = "Allow publish workflows to run with a dirty working tree"
1884 )]
1885 pub allow_dirty: bool,
1886 #[arg(long, help = "Skip configured preflight commands and publish anyway")]
1887 pub force: bool,
1888 #[arg(
1889 long,
1890 help = "When publishing a crate workspace member, also publish missing internal prerequisites in dependency order"
1891 )]
1892 pub include_prereqs: bool,
1893 #[arg(long, help = "Limit publishing to one target: npm or crates")]
1894 pub target: Option<String>,
1895 #[arg(
1896 long,
1897 help = "Publish the npm package or crate belonging to this configured service"
1898 )]
1899 pub service: Option<String>,
1900 #[arg(
1901 long,
1902 help = "Limit publishing to the workflow whose manifest matches this path"
1903 )]
1904 pub manifest_path: Option<PathBuf>,
1905}
1906
1907#[derive(Args, Debug)]
1908#[command(
1909 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1910 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"
1911)]
1912pub struct VersionWorkspaceCmd {
1913 #[command(subcommand)]
1914 pub command: VersionWorkspaceSubCommand,
1915}
1916
1917#[derive(Args, Debug)]
1918#[command(
1919 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1920 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"
1921)]
1922pub struct VersionDomainCmd {
1923 #[command(subcommand)]
1924 pub command: VersionDomainSubCommand,
1925}
1926
1927#[derive(Subcommand, Debug)]
1928pub enum VersionDomainSubCommand {
1929 #[command(about = "Discover or scaffold a version-domain policy")]
1930 Init(VersionDomainInitCmd),
1931 #[command(about = "Validate release-domain ownership and configured surfaces")]
1932 Doctor(VersionDomainDoctorCmd),
1933 #[command(about = "Preview or apply version-domain surface synchronization")]
1934 Sync(VersionDomainSyncCmd),
1935 #[command(about = "Classify build logs for release-domain drift and unsafe alignments")]
1936 Diagnose(VersionDomainDiagnoseCmd),
1937 #[command(about = "Run a guarded domain version bump/set and optional deploy")]
1938 Release(VersionDomainReleaseCmd),
1939}
1940
1941#[derive(Args, Debug)]
1942pub struct VersionDomainInitCmd {
1943 #[arg(long, help = "Release domain name")]
1944 pub name: String,
1945 #[arg(long, help = "Domain root path relative to the XBP project root")]
1946 pub root: PathBuf,
1947 #[arg(long, help = "Persist the generated domain to .xbp/xbp.toml")]
1948 pub write: bool,
1949}
1950
1951#[derive(Args, Debug)]
1952pub struct VersionDomainDoctorCmd {
1953 #[arg(long = "domain", help = "Release domain name")]
1954 pub domain: String,
1955}
1956
1957#[derive(Args, Debug)]
1958pub struct VersionDomainSyncCmd {
1959 #[arg(long = "domain", help = "Release domain name")]
1960 pub domain: String,
1961 #[arg(
1962 long,
1963 conflicts_with = "write",
1964 help = "Validate only; do not write files"
1965 )]
1966 pub check: bool,
1967 #[arg(long, conflicts_with = "check", help = "Write configured surfaces")]
1968 pub write: bool,
1969}
1970
1971#[derive(Args, Debug)]
1972pub struct VersionDomainDiagnoseCmd {
1973 #[arg(long = "domain", help = "Release domain name")]
1974 pub domain: String,
1975 #[arg(long, help = "Build or deploy log file to classify")]
1976 pub log: Option<PathBuf>,
1977}
1978
1979#[derive(Args, Debug)]
1980#[command(group(
1981 clap::ArgGroup::new("domain_release_version")
1982 .required(true)
1983 .args(["patch", "minor", "major", "version"])
1984))]
1985pub struct VersionDomainReleaseCmd {
1986 #[arg(long = "domain", help = "Release domain name")]
1987 pub domain: String,
1988 #[arg(long, help = "Bump the domain patch version")]
1989 pub patch: bool,
1990 #[arg(long, help = "Bump the domain minor version")]
1991 pub minor: bool,
1992 #[arg(long, help = "Bump the domain major version")]
1993 pub major: bool,
1994 #[arg(long, help = "Set an explicit domain version")]
1995 pub version: Option<String>,
1996 #[arg(long, help = "Run the configured Cloudflare app release after sync")]
1997 pub deploy: bool,
1998 #[arg(long, help = "Allow an otherwise blocked major-version jump")]
1999 pub allow_major_jump: bool,
2000 #[arg(
2001 long,
2002 value_name = "DOMAIN",
2003 help = "Explicitly record an allowed cross-domain version source"
2004 )]
2005 pub allow_cross_domain_version: Option<String>,
2006}
2007
2008#[derive(Args, Debug, Clone, Default)]
2009pub struct VersionWorkspaceTargetArgs {
2010 #[arg(
2011 long,
2012 help = "Workspace repo root to inspect (defaults to current project root)"
2013 )]
2014 pub repo: Option<PathBuf>,
2015 #[arg(long, help = "Emit machine-readable JSON output")]
2016 pub json: bool,
2017}
2018
2019#[derive(Subcommand, Debug)]
2020pub enum VersionWorkspaceSubCommand {
2021 #[command(about = "Detect workspace release drift and exit non-zero when mismatches exist")]
2022 Check(VersionWorkspaceCheckCmd),
2023 #[command(about = "Preview or apply workspace-wide version alignment")]
2024 Sync(VersionWorkspaceSyncCmd),
2025 #[command(about = "Run structural and optional cargo validation for workspace publishability")]
2026 Validate(VersionWorkspaceValidateCmd),
2027 #[command(about = "Plan or execute crates.io publishing for workspace packages")]
2028 Publish(VersionWorkspacePublishCmd),
2029}
2030
2031#[derive(Args, Debug)]
2032pub struct VersionWorkspaceCheckCmd {
2033 #[command(flatten)]
2034 pub target: VersionWorkspaceTargetArgs,
2035 #[arg(
2036 long,
2037 help = "Expected release version (defaults to the root package version)"
2038 )]
2039 pub version: Option<String>,
2040}
2041
2042#[derive(Args, Debug)]
2043pub struct VersionWorkspaceSyncCmd {
2044 #[command(flatten)]
2045 pub target: VersionWorkspaceTargetArgs,
2046 #[arg(
2047 long,
2048 help = "Target release version (defaults to the root package version)"
2049 )]
2050 pub version: Option<String>,
2051 #[arg(
2052 long,
2053 help = "Write changes to disk instead of previewing the sync plan"
2054 )]
2055 pub write: bool,
2056}
2057
2058#[derive(Args, Debug)]
2059pub struct VersionWorkspaceValidateCmd {
2060 #[command(flatten)]
2061 pub target: VersionWorkspaceTargetArgs,
2062 #[arg(long, help = "Limit cargo validation to a single package name")]
2063 pub package: Option<String>,
2064 #[arg(long, help = "Run `cargo check -q` as part of validation")]
2065 pub cargo_check: bool,
2066 #[arg(
2067 long,
2068 help = "Run `cargo publish --dry-run --locked` for publishable packages"
2069 )]
2070 pub package_dry_run: bool,
2071}
2072
2073#[derive(Args, Debug)]
2074#[command(arg_required_else_help = true)]
2075pub struct VersionWorkspacePublishCmd {
2076 #[command(subcommand)]
2077 pub command: VersionWorkspacePublishSubCommand,
2078}
2079
2080#[derive(Subcommand, Debug)]
2081pub enum VersionWorkspacePublishSubCommand {
2082 #[command(about = "Show publish order, crates.io visibility, and blockers without publishing")]
2083 Plan(VersionWorkspacePublishPlanCmd),
2084 #[command(
2085 about = "Inspect or auto-fix Cargo publish surface issues (publish=false, version pins, metadata)"
2086 )]
2087 Heal(VersionWorkspacePublishHealCmd),
2088 #[command(about = "Publish workspace packages in dependency order")]
2089 Run(VersionWorkspacePublishRunCmd),
2090}
2091
2092#[derive(Args, Debug)]
2093pub struct VersionWorkspacePublishHealCmd {
2094 #[command(flatten)]
2095 pub target: VersionWorkspaceTargetArgs,
2096 #[arg(long, help = "Limit healing to the publish closure of one package")]
2097 pub only: Option<String>,
2098 #[arg(
2099 long,
2100 help = "When healing a single package, also include internal path-dependency prerequisites"
2101 )]
2102 pub include_prereqs: bool,
2103 #[arg(long, help = "Write Cargo.toml fixes (default is dry preview)")]
2104 pub write: bool,
2105 #[arg(
2106 long,
2107 help = "Expected workspace version for pin alignment (default: highest package version / config)"
2108 )]
2109 pub version: Option<String>,
2110}
2111
2112#[derive(Args, Debug)]
2113pub struct VersionWorkspacePublishPlanCmd {
2114 #[command(flatten)]
2115 pub target: VersionWorkspaceTargetArgs,
2116 #[arg(long, help = "Limit the plan to one package")]
2117 pub only: Option<String>,
2118 #[arg(
2119 long,
2120 help = "When planning a single package, also include missing internal prerequisites"
2121 )]
2122 pub include_prereqs: bool,
2123}
2124
2125#[derive(Args, Debug)]
2126pub struct VersionWorkspacePublishRunCmd {
2127 #[command(flatten)]
2128 pub target: VersionWorkspaceTargetArgs,
2129 #[arg(long, help = "Preview publish actions without calling cargo publish")]
2130 pub dry_run: bool,
2131 #[arg(
2132 long,
2133 help = "Start publishing from this package in the computed order"
2134 )]
2135 pub from: Option<String>,
2136 #[arg(long, help = "Publish only this package")]
2137 pub only: Option<String>,
2138 #[arg(
2139 long,
2140 help = "When publishing one package, also publish missing internal prerequisites in dependency order"
2141 )]
2142 pub include_prereqs: bool,
2143 #[arg(long, help = "Continue publishing remaining packages after a failure")]
2144 pub continue_on_error: bool,
2145 #[arg(long, help = "Allow publishing from a dirty worktree")]
2146 pub allow_dirty: bool,
2147 #[arg(
2148 long,
2149 default_value_t = true,
2150 action = clap::ArgAction::Set,
2151 help = "Auto-fix publish-surface issues (publish=false, missing version pins) before publishing"
2152 )]
2153 pub auto_fix: bool,
2154 #[arg(
2155 long,
2156 default_value_t = 180.0,
2157 help = "How long to wait for each published version to become visible on crates.io"
2158 )]
2159 pub timeout_seconds: f64,
2160 #[arg(
2161 long,
2162 default_value_t = 5.0,
2163 help = "How often to poll crates.io for the just-published version"
2164 )]
2165 pub poll_interval_seconds: f64,
2166}
2167
2168#[derive(Args, Debug)]
2169pub struct RedeployV2Cmd {
2170 #[arg(short = 'p', long = "password")]
2171 pub password: Option<String>,
2172 #[arg(short = 'u', long = "username")]
2173 pub username: Option<String>,
2174 #[arg(short = 'h', long = "host")]
2175 pub host: Option<String>,
2176 #[arg(short = 'd', long = "project-dir")]
2177 pub project_dir: Option<String>,
2178}
2179
2180#[derive(Args, Debug)]
2181#[command(
2182 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2183 after_help = crate::cli::help_render::LOGS_AFTER_HELP
2184)]
2185pub struct LogsCmd {
2186 #[arg()]
2187 pub project: Option<String>,
2188 #[arg(long = "ssh-host", help = "SSH host to stream logs from")]
2189 pub ssh_host: Option<String>,
2190 #[arg(long = "ssh-username", help = "SSH username for remote host")]
2191 pub ssh_username: Option<String>,
2192 #[arg(long = "ssh-password", help = "SSH password for remote host")]
2193 pub ssh_password: Option<String>,
2194}
2195
2196#[derive(Args, Debug)]
2197#[command(
2198 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2199 after_help = crate::cli::help_render::SSH_AFTER_HELP
2200)]
2201pub struct SshCmd {
2202 #[arg(long = "host", alias = "ssh-host", help = "SSH host or IP address")]
2203 pub ssh_host: Option<String>,
2204 #[arg(
2205 long = "port",
2206 default_value_t = 22,
2207 help = "SSH port for direct connections"
2208 )]
2209 pub ssh_port: u16,
2210 #[arg(
2211 long = "username",
2212 alias = "ssh-username",
2213 help = "SSH username for the remote host"
2214 )]
2215 pub ssh_username: Option<String>,
2216 #[arg(
2217 long = "password",
2218 alias = "ssh-password",
2219 help = "SSH password (omit to use stored config or a secure prompt)"
2220 )]
2221 pub ssh_password: Option<String>,
2222 #[arg(
2223 long,
2224 help = "Path to a private key file to use instead of password auth"
2225 )]
2226 pub private_key: Option<PathBuf>,
2227 #[arg(long, help = "Passphrase for --private-key when required")]
2228 pub private_key_passphrase: Option<String>,
2229 #[arg(
2230 long,
2231 help = "Run this remote command in a PTY instead of opening the default login shell"
2232 )]
2233 pub command: Option<String>,
2234 #[arg(
2235 long,
2236 help = "TERM value sent to the server (default: TERM env var or xterm-256color)"
2237 )]
2238 pub term: Option<String>,
2239 #[arg(long, help = "Disable SSH host key verification")]
2240 pub no_host_key_check: bool,
2241 #[arg(
2242 long,
2243 help = "Pin the SSH host key as a base64 blob when using tunnels or first-connect flows"
2244 )]
2245 pub host_key: Option<String>,
2246 #[arg(
2247 long,
2248 help = "Path to a known_hosts file used for SSH host verification"
2249 )]
2250 pub known_hosts_file: Option<PathBuf>,
2251 #[arg(
2252 long,
2253 help = "Cloudflare Access hostname used to open a local cloudflared TCP forwarder"
2254 )]
2255 pub cloudflared_hostname: Option<String>,
2256 #[arg(long, help = "Override the cloudflared binary path")]
2257 pub cloudflared_binary: Option<PathBuf>,
2258 #[arg(
2259 long,
2260 help = "Optional destination host:port passed to cloudflared access tcp"
2261 )]
2262 pub cloudflared_destination: Option<String>,
2263}
2264
2265#[derive(Args, Debug)]
2266#[command(
2267 arg_required_else_help = true,
2268 disable_help_subcommand = true,
2269 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
2270)]
2271pub struct CloudflaredCmd {
2272 #[command(subcommand)]
2273 pub command: CloudflaredSubCommand,
2274}
2275
2276#[derive(Subcommand, Debug)]
2277pub enum CloudflaredSubCommand {
2278 #[command(about = "Start a local cloudflared Access TCP forwarder")]
2279 Tcp(CloudflaredTcpCmd),
2280}
2281
2282#[derive(Args, Debug)]
2283#[command(
2284 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2285 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"
2286)]
2287pub struct CloudflaredTcpCmd {
2288 #[arg(long, help = "Protected Cloudflare Access hostname")]
2289 pub hostname: Option<String>,
2290 #[arg(
2291 long,
2292 help = "Local listener address for the forwarder (default: auto-allocate 127.0.0.1:<port>)"
2293 )]
2294 pub listener: Option<String>,
2295 #[arg(
2296 long,
2297 help = "Optional destination host:port passed to cloudflared access tcp"
2298 )]
2299 pub destination: Option<String>,
2300 #[arg(long, help = "Override the cloudflared binary path")]
2301 pub binary: Option<PathBuf>,
2302}
2303
2304#[derive(Args, Debug)]
2305#[command(
2306 arg_required_else_help = true,
2307 disable_help_subcommand = true,
2308 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2309 after_help = crate::cli::help_render::NGINX_AFTER_HELP
2310)]
2311pub struct NginxCmd {
2312 #[command(subcommand)]
2313 pub command: NginxSubCommand,
2314}
2315
2316#[derive(Args, Debug)]
2317#[command(
2318 arg_required_else_help = true,
2319 disable_help_subcommand = true,
2320 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
2321)]
2322pub struct NetworkCmd {
2323 #[command(subcommand)]
2324 pub command: NetworkSubCommand,
2325}
2326
2327#[derive(Subcommand, Debug)]
2328pub enum NetworkSubCommand {
2329 #[command(about = "Manage persistent floating IP configuration")]
2330 FloatingIp(NetworkFloatingIpCmd),
2331 #[command(about = "Inspect discovered network configuration sources")]
2332 Config(NetworkConfigCmd),
2333 #[command(about = "Manage Hetzner-specific Linux network configuration")]
2334 Hetzner(NetworkHetznerCmd),
2335}
2336
2337#[derive(Args, Debug)]
2338pub struct NetworkFloatingIpCmd {
2339 #[command(subcommand)]
2340 pub command: NetworkFloatingIpSubCommand,
2341}
2342
2343#[derive(Subcommand, Debug)]
2344pub enum NetworkFloatingIpSubCommand {
2345 #[command(about = "Add a persistent floating IP entry to detected network backend")]
2346 Add {
2347 #[arg(long, help = "Floating IP address (IPv4 or IPv6)")]
2348 ip: String,
2349 #[arg(long, help = "CIDR suffix (defaults: IPv4=32, IPv6=64)")]
2350 cidr: Option<u8>,
2351 #[arg(long, help = "Network interface override (auto-detected when omitted)")]
2352 interface: Option<String>,
2353 #[arg(long, help = "Optional label for backend metadata/file naming")]
2354 label: Option<String>,
2355 #[arg(long, help = "Apply network changes after writing config")]
2356 apply: bool,
2357 #[arg(long, help = "Preview computed changes without writing files")]
2358 dry_run: bool,
2359 },
2360 #[command(about = "List floating IPs from runtime and persisted network config")]
2361 List {
2362 #[arg(long, help = "Emit JSON output")]
2363 json: bool,
2364 },
2365}
2366
2367#[derive(Args, Debug)]
2368pub struct NetworkConfigCmd {
2369 #[command(subcommand)]
2370 pub command: NetworkConfigSubCommand,
2371}
2372
2373#[derive(Subcommand, Debug)]
2374pub enum NetworkConfigSubCommand {
2375 #[command(about = "List detected backend and configuration source files")]
2376 List {
2377 #[arg(long, help = "Emit JSON output")]
2378 json: bool,
2379 },
2380}
2381
2382#[derive(Args, Debug)]
2383pub struct NetworkHetznerCmd {
2384 #[command(subcommand)]
2385 pub command: NetworkHetznerSubCommand,
2386}
2387
2388#[derive(Subcommand, Debug)]
2389pub enum NetworkHetznerSubCommand {
2390 #[command(about = "Configure a Hetzner vSwitch VLAN interface persistently")]
2391 Vswitch(NetworkHetznerVswitchCmd),
2392}
2393
2394#[derive(Args, Debug)]
2395pub struct NetworkHetznerVswitchCmd {
2396 #[command(subcommand)]
2397 pub command: NetworkHetznerVswitchSubCommand,
2398}
2399
2400#[derive(Subcommand, Debug)]
2401pub enum NetworkHetznerVswitchSubCommand {
2402 #[command(about = "Write persistent Linux config for a Hetzner vSwitch VLAN interface")]
2403 Setup {
2404 #[arg(
2405 long,
2406 help = "Private IPv4 address to assign on the vSwitch VLAN interface"
2407 )]
2408 ip: String,
2409 #[arg(
2410 long,
2411 default_value_t = 24,
2412 help = "CIDR prefix for --ip (default: 24)"
2413 )]
2414 cidr: u8,
2415 #[arg(long, help = "Physical parent interface (auto-detected when omitted)")]
2416 interface: Option<String>,
2417 #[arg(long, help = "Hetzner vSwitch VLAN ID")]
2418 vlan_id: u16,
2419 #[arg(long, default_value_t = 1400, help = "Interface MTU (default: 1400)")]
2420 mtu: u16,
2421 #[arg(
2422 long,
2423 default_value = "10.0.3.1",
2424 help = "Gateway for the routed Hetzner cloud network"
2425 )]
2426 gateway: String,
2427 #[arg(
2428 long,
2429 default_value = "10.0.0.0/16",
2430 help = "Destination CIDR routed through the Hetzner vSwitch gateway"
2431 )]
2432 route_cidr: String,
2433 #[arg(long, help = "Apply or activate the new config immediately")]
2434 apply: bool,
2435 #[arg(long, help = "Preview file changes without writing them")]
2436 dry_run: bool,
2437 },
2438}
2439
2440#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
2441pub enum NginxDnsMode {
2442 Manual,
2443 Plugin,
2444}
2445
2446#[derive(Subcommand, Debug)]
2447pub enum NginxSubCommand {
2448 #[command(
2449 about = "Provision an HTTPS NGINX reverse proxy with Certbot",
2450 long_about = "Provision an NGINX reverse proxy, issue or reuse Let's Encrypt certificates,\n\
2451and write final HTTP->HTTPS redirect + TLS proxy config.\n\
2452\n\
2453Wildcard domains (for example *.example.com) require DNS-01 mode.\n\
2454Use --dns-mode manual for interactive TXT record prompts, or --dns-mode plugin\n\
2455with --dns-plugin and --dns-creds for non-interactive provider automation."
2456 )]
2457 Setup {
2458 #[arg(short, long, help = "Domain name (supports wildcard: *.example.com)")]
2459 domain: String,
2460 #[arg(short, long, help = "Port to proxy to")]
2461 port: u16,
2462 #[arg(
2463 short,
2464 long,
2465 help = "Email used for Let's Encrypt account registration"
2466 )]
2467 email: String,
2468 #[arg(
2469 long,
2470 value_enum,
2471 default_value_t = NginxDnsMode::Manual,
2472 help = "DNS challenge mode for wildcard certificates: manual or plugin"
2473 )]
2474 dns_mode: NginxDnsMode,
2475 #[arg(
2476 long,
2477 help = "Certbot DNS plugin name for --dns-mode plugin (for example: cloudflare)"
2478 )]
2479 dns_plugin: Option<String>,
2480 #[arg(
2481 long,
2482 help = "Path to DNS plugin credentials file for --dns-mode plugin"
2483 )]
2484 dns_creds: Option<PathBuf>,
2485 #[arg(
2486 long,
2487 default_value_t = true,
2488 action = clap::ArgAction::Set,
2489 value_parser = clap::builder::BoolishValueParser::new(),
2490 help = "For wildcard domains, also request the base domain certificate (true|false)"
2491 )]
2492 include_base: bool,
2493 },
2494 #[command(about = "List discovered NGINX sites with listen/upstream ports")]
2495 List,
2496 #[command(about = "Show full NGINX config for one domain or all domains")]
2497 Show {
2498 #[arg(help = "Optional domain name to inspect")]
2499 domain: Option<String>,
2500 },
2501 #[command(about = "Open an NGINX site config in your configured editor")]
2502 Edit {
2503 #[arg(help = "Domain name to edit")]
2504 domain: String,
2505 },
2506 #[command(about = "Update upstream port for an existing NGINX site")]
2507 Update {
2508 #[arg(short, long, help = "Domain name to update")]
2509 domain: String,
2510 #[arg(short, long, help = "New port to proxy to")]
2511 port: u16,
2512 },
2513}
2514
2515#[derive(Args, Debug)]
2516#[command(
2517 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2518 after_help = crate::cli::help_render::DIAG_AFTER_HELP
2519)]
2520pub struct DiagCmd {
2521 #[arg(long, help = "Only check Nginx configuration (legacy focus mode)")]
2522 pub nginx: bool,
2523 #[arg(
2524 long,
2525 help = "Interactively offer to fix Nginx proxy_pass mismatches (default: report only)"
2526 )]
2527 pub fix_nginx: bool,
2528 #[arg(long, hide = true)]
2529 pub refresh_system_inventory: bool,
2530 #[arg(
2531 long,
2532 help = "Refresh and print persisted machine inventory in the global XBP config"
2533 )]
2534 pub codetime: bool,
2535 #[arg(
2536 long,
2537 help = "Inspect Cursor roaming data on Windows; implies --codetime"
2538 )]
2539 pub cursor: bool,
2540 #[arg(long, help = "Check specific ports (comma-separated)")]
2541 pub ports: Option<String>,
2542 #[arg(long, help = "Skip internet speed test")]
2543 pub no_speed_test: bool,
2544 #[arg(
2545 long,
2546 help = "Faster run: skip speed test, HTTP probes, and DNS deep checks"
2547 )]
2548 pub quick: bool,
2549 #[arg(
2550 long,
2551 help = "Expanded run: HTTP health probes, DNS, git hygiene, CLI session, Docker status"
2552 )]
2553 pub full: bool,
2554 #[arg(
2555 long,
2556 help = "Probe configured service ports over HTTP (also enabled by --full)"
2557 )]
2558 pub http_probe: bool,
2559 #[arg(long, help = "Print the full diagnostic report as JSON")]
2560 pub json: bool,
2561 #[arg(
2562 long,
2563 help = "Path to docker compose file to validate (defaults to docker-compose.yml/compose.yml)"
2564 )]
2565 pub compose_file: Option<String>,
2566}
2567
2568#[derive(Args, Debug)]
2569pub struct MonitorCmd {
2570 #[command(subcommand)]
2571 pub command: Option<MonitorSubCommand>,
2572}
2573
2574#[derive(Subcommand, Debug)]
2575pub enum MonitorSubCommand {
2576 Check,
2577 Start,
2578}
2579
2580#[cfg(feature = "monitoring")]
2581#[derive(Args, Debug)]
2582pub struct MonitoringCmd {
2583 #[command(subcommand)]
2584 pub command: MonitoringSubCommand,
2585}
2586
2587#[cfg(feature = "monitoring")]
2588#[derive(Subcommand, Debug)]
2589pub enum MonitoringSubCommand {
2590 Serve {
2591 #[arg(
2592 short,
2593 long,
2594 default_value = "prodzilla.yml",
2595 help = "Monitoring config file"
2596 )]
2597 file: String,
2598 },
2599 RunOnce {
2600 #[arg(
2601 short,
2602 long,
2603 default_value = "prodzilla.yml",
2604 help = "Monitoring config file"
2605 )]
2606 file: String,
2607 #[arg(long, help = "Run probes only")]
2608 probes_only: bool,
2609 #[arg(long, help = "Run stories only")]
2610 stories_only: bool,
2611 },
2612 List {
2613 #[arg(
2614 short,
2615 long,
2616 default_value = "prodzilla.yml",
2617 help = "Monitoring config file"
2618 )]
2619 file: String,
2620 },
2621}
2622
2623#[derive(Args, Debug)]
2624#[command(
2625 arg_required_else_help = true,
2626 disable_help_subcommand = true,
2627 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2628 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."
2629)]
2630pub struct ApiCmd {
2631 #[command(subcommand)]
2632 pub command: ApiSubCommand,
2633}
2634
2635#[derive(Args, Debug)]
2636#[command(
2637 arg_required_else_help = true,
2638 disable_help_subcommand = true,
2639 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2640 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>"
2641)]
2642pub struct RunnersCmd {
2643 #[command(subcommand)]
2644 pub command: RunnersSubCommand,
2645}
2646
2647#[derive(Subcommand, Debug)]
2648pub enum RunnersSubCommand {
2649 #[command(about = "List organizations already registered in the XBP control plane")]
2650 OrgsList(RunnersOrgsListCmd),
2651 #[command(about = "Manage runner groups")]
2652 Groups(RunnersGroupsCmd),
2653 #[command(about = "List or enroll runner hosts")]
2654 Hosts(RunnersHostsCmd),
2655 #[command(about = "List runner inventory for an organization")]
2656 Inventory(RunnersInventoryCmd),
2657 #[command(about = "Show runner status for an organization or host")]
2658 Status(RunnersStatusCmd),
2659 #[command(about = "Show recent runner job logs and failures")]
2660 Logs(RunnersLogsCmd),
2661 #[command(about = "Queue a runner sync job")]
2662 Sync(RunnersSyncCmd),
2663 #[command(about = "Queue a runner deployment job")]
2664 Deploy(RunnersDeployCmd),
2665 #[command(about = "Queue a runner update job")]
2666 Update(RunnersUpdateCmd),
2667 #[command(about = "Queue a runner removal job")]
2668 Remove(RunnersRemoveCmd),
2669 #[command(about = "Run a local runner host poller that claims and executes jobs")]
2670 Agent(RunnersAgentCmd),
2671}
2672
2673#[derive(Args, Debug)]
2674pub struct RunnersOrgsListCmd {
2675 #[command(flatten)]
2676 pub target: ApiTargetOptions,
2677}
2678
2679#[derive(Args, Debug)]
2680pub struct RunnersGroupsCmd {
2681 #[command(subcommand)]
2682 pub command: RunnersGroupsSubCommand,
2683}
2684
2685#[derive(Subcommand, Debug)]
2686pub enum RunnersGroupsSubCommand {
2687 #[command(about = "List runner groups for an organization")]
2688 List(RunnersGroupsListCmd),
2689 #[command(about = "Create or upsert a runner group")]
2690 Create(RunnersGroupsCreateCmd),
2691 #[command(about = "Update an existing runner group")]
2692 Update(RunnersGroupsCreateCmd),
2693 #[command(about = "Delete a runner group")]
2694 Delete(RunnersGroupsDeleteCmd),
2695 #[command(about = "Inspect effective repository access for a runner group")]
2696 Access(RunnersGroupsAccessCmd),
2697}
2698
2699#[derive(Args, Debug)]
2700pub struct RunnersGroupsListCmd {
2701 #[arg(long, help = "Organization ID")]
2702 pub organization_id: String,
2703 #[command(flatten)]
2704 pub target: ApiTargetOptions,
2705}
2706
2707#[derive(Args, Debug, Clone)]
2708pub struct RunnersGroupsCreateCmd {
2709 #[arg(long, help = "Organization ID")]
2710 pub organization_id: String,
2711 #[arg(long, help = "Runner group name")]
2712 pub name: String,
2713 #[arg(long, help = "Visibility: all, selected, or private")]
2714 pub visibility: Option<String>,
2715 #[arg(long, help = "Optional GitHub installation ID")]
2716 pub github_installation_id: Option<String>,
2717 #[arg(long, help = "Optional GitHub runner group ID")]
2718 pub github_runner_group_id: Option<i64>,
2719 #[arg(long, help = "Optional sync state")]
2720 pub sync_state: Option<String>,
2721 #[arg(long, help = "Mark group as inherited")]
2722 pub inherited: bool,
2723 #[arg(long, help = "Allow public repositories")]
2724 pub allows_public_repositories: bool,
2725 #[arg(long, help = "Prompt for visibility and repository access settings")]
2726 pub interactive: bool,
2727 #[arg(long, help = "Restrict group to workflows")]
2728 pub restricted_to_workflows: bool,
2729 #[arg(long, help = "Selected workflows JSON array")]
2730 pub selected_workflows_json: Option<String>,
2731 #[arg(long, help = "Metadata JSON object")]
2732 pub metadata_json: Option<String>,
2733 #[command(flatten)]
2734 pub target: ApiTargetOptions,
2735}
2736
2737#[derive(Args, Debug)]
2738pub struct RunnersGroupsDeleteCmd {
2739 #[arg(help = "Runner group ID")]
2740 pub runner_group_id: String,
2741 #[command(flatten)]
2742 pub target: ApiTargetOptions,
2743}
2744
2745#[derive(Args, Debug)]
2746pub struct RunnersGroupsAccessCmd {
2747 #[arg(help = "Runner group ID")]
2748 pub runner_group_id: String,
2749 #[command(flatten)]
2750 pub target: ApiTargetOptions,
2751}
2752
2753#[derive(Args, Debug)]
2754pub struct RunnersHostsCmd {
2755 #[command(subcommand)]
2756 pub command: RunnersHostsSubCommand,
2757}
2758
2759#[derive(Subcommand, Debug)]
2760pub enum RunnersHostsSubCommand {
2761 #[command(about = "List runner hosts")]
2762 List(RunnersHostsListCmd),
2763 #[command(about = "Enroll or upsert a runner host")]
2764 Enroll(Box<RunnersHostsEnrollCmd>),
2765 #[command(about = "Run host preflight checks and persist the result")]
2766 Preflight(RunnersHostsPreflightCmd),
2767 #[command(about = "Inspect host enrollment, heartbeat, and preflight state")]
2768 Status(RunnersHostsStatusCmd),
2769}
2770
2771#[derive(Args, Debug)]
2772pub struct RunnersHostsListCmd {
2773 #[arg(long, help = "Optional organization ID")]
2774 pub organization_id: Option<String>,
2775 #[arg(long, help = "Optional daemon ID")]
2776 pub daemon_id: Option<String>,
2777 #[arg(long, help = "Optional status filter")]
2778 pub status: Option<String>,
2779 #[command(flatten)]
2780 pub target: ApiTargetOptions,
2781}
2782
2783#[derive(Args, Debug)]
2784pub struct RunnersHostsEnrollCmd {
2785 #[arg(long, help = "Organization ID")]
2786 pub organization_id: String,
2787 #[arg(long, help = "Optional daemon ID for daemon-backed hosts")]
2788 pub daemon_id: Option<String>,
2789 #[arg(long, help = "Host kind: daemon or rogue")]
2790 pub host_kind: String,
2791 #[arg(long, help = "Platform: linux, windows, or macos")]
2792 pub platform: String,
2793 #[arg(long, help = "Hostname")]
2794 pub hostname: String,
2795 #[arg(long, help = "Runner name prefix")]
2796 pub runner_name_prefix: String,
2797 #[arg(long, help = "Optional display name")]
2798 pub display_name: Option<String>,
2799 #[arg(long, help = "Optional architecture")]
2800 pub arch: Option<String>,
2801 #[arg(long, help = "Optional status")]
2802 pub status: Option<String>,
2803 #[arg(long, help = "Labels JSON object")]
2804 pub labels_json: Option<String>,
2805 #[arg(long, help = "Capabilities JSON object")]
2806 pub capabilities_json: Option<String>,
2807 #[arg(long, help = "Metadata JSON object")]
2808 pub metadata_json: Option<String>,
2809 #[command(flatten)]
2810 pub target: ApiTargetOptions,
2811}
2812
2813#[derive(Args, Debug)]
2814pub struct RunnersHostsPreflightCmd {
2815 #[arg(long, help = "Runner host ID")]
2816 pub runner_host_id: String,
2817 #[arg(
2818 long,
2819 help = "Persist the preflight result back to the control-plane API"
2820 )]
2821 pub write_api: bool,
2822 #[command(flatten)]
2823 pub target: ApiTargetOptions,
2824}
2825
2826#[derive(Args, Debug)]
2827pub struct RunnersHostsStatusCmd {
2828 #[arg(long, help = "Runner host ID")]
2829 pub runner_host_id: String,
2830 #[command(flatten)]
2831 pub target: ApiTargetOptions,
2832}
2833
2834#[derive(Args, Debug)]
2835pub struct RunnersInventoryCmd {
2836 #[arg(long, help = "Organization ID")]
2837 pub organization_id: String,
2838 #[command(flatten)]
2839 pub target: ApiTargetOptions,
2840}
2841
2842#[derive(Args, Debug)]
2843pub struct RunnersStatusCmd {
2844 #[arg(long, help = "Organization ID")]
2845 pub organization_id: String,
2846 #[arg(long, help = "Optional runner host ID")]
2847 pub runner_host_id: Option<String>,
2848 #[arg(long, help = "Optional runner ID")]
2849 pub runner_id: Option<String>,
2850 #[arg(long, help = "Optional daemon ID")]
2851 pub daemon_id: Option<String>,
2852 #[arg(long, help = "Optional status filter")]
2853 pub status: Option<String>,
2854 #[arg(long, help = "Optional phase filter")]
2855 pub phase: Option<String>,
2856 #[arg(long, default_value_t = 20, help = "Maximum jobs to show")]
2857 pub limit: usize,
2858 #[command(flatten)]
2859 pub target: ApiTargetOptions,
2860}
2861
2862#[derive(Args, Debug)]
2863pub struct RunnersLogsCmd {
2864 #[arg(long, help = "Optional organization ID")]
2865 pub organization_id: Option<String>,
2866 #[arg(long, help = "Optional runner host ID")]
2867 pub runner_host_id: Option<String>,
2868 #[arg(long, help = "Optional runner ID")]
2869 pub runner_id: Option<String>,
2870 #[arg(long, help = "Optional daemon ID")]
2871 pub daemon_id: Option<String>,
2872 #[arg(long, help = "Optional status filter")]
2873 pub status: Option<String>,
2874 #[arg(long, help = "Optional phase filter")]
2875 pub phase: Option<String>,
2876 #[arg(long, default_value_t = 10, help = "Maximum jobs to show")]
2877 pub limit: usize,
2878 #[command(flatten)]
2879 pub target: ApiTargetOptions,
2880}
2881
2882#[derive(Args, Debug)]
2883pub struct RunnersSyncCmd {
2884 #[arg(long, help = "Organization ID")]
2885 pub organization_id: String,
2886 #[arg(long, help = "Optional runner host ID")]
2887 pub runner_host_id: Option<String>,
2888 #[arg(long, help = "Optional daemon ID")]
2889 pub daemon_id: Option<String>,
2890 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
2891 pub run_after: Option<String>,
2892 #[arg(long, help = "Payload JSON object")]
2893 pub payload_json: Option<String>,
2894 #[command(flatten)]
2895 pub target: ApiTargetOptions,
2896}
2897
2898#[derive(Args, Debug)]
2899pub struct RunnersDeployCmd {
2900 #[arg(long, help = "Organization ID")]
2901 pub organization_id: String,
2902 #[arg(long, help = "Optional runner host ID")]
2903 pub runner_host_id: Option<String>,
2904 #[arg(long, help = "Optional daemon ID")]
2905 pub daemon_id: Option<String>,
2906 #[arg(long, help = "Optional existing runner ID")]
2907 pub runner_id: Option<String>,
2908 #[arg(long, help = "Optional priority")]
2909 pub priority: Option<i32>,
2910 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
2911 pub run_after: Option<String>,
2912 #[arg(long, help = "Payload JSON object")]
2913 pub payload_json: Option<String>,
2914 #[command(flatten)]
2915 pub target: ApiTargetOptions,
2916}
2917
2918#[derive(Args, Debug)]
2919pub struct RunnersUpdateCmd {
2920 #[arg(long, help = "Organization ID")]
2921 pub organization_id: String,
2922 #[arg(long, help = "Optional runner host ID")]
2923 pub runner_host_id: Option<String>,
2924 #[arg(long, help = "Optional daemon ID")]
2925 pub daemon_id: Option<String>,
2926 #[arg(long, help = "Optional existing runner ID")]
2927 pub runner_id: Option<String>,
2928 #[arg(long, help = "Optional priority")]
2929 pub priority: Option<i32>,
2930 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
2931 pub run_after: Option<String>,
2932 #[arg(long, help = "Payload JSON object")]
2933 pub payload_json: Option<String>,
2934 #[command(flatten)]
2935 pub target: ApiTargetOptions,
2936}
2937
2938#[derive(Args, Debug)]
2939pub struct RunnersRemoveCmd {
2940 #[arg(long, help = "Organization ID")]
2941 pub organization_id: String,
2942 #[arg(long, help = "Existing runner ID")]
2943 pub runner_id: String,
2944 #[arg(long, help = "Optional runner host ID")]
2945 pub runner_host_id: Option<String>,
2946 #[arg(long, help = "Optional daemon ID")]
2947 pub daemon_id: Option<String>,
2948 #[arg(long, help = "Optional priority")]
2949 pub priority: Option<i32>,
2950 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
2951 pub run_after: Option<String>,
2952 #[arg(long, help = "Payload JSON object")]
2953 pub payload_json: Option<String>,
2954 #[command(flatten)]
2955 pub target: ApiTargetOptions,
2956}
2957
2958#[derive(Args, Debug)]
2959pub struct RunnersAgentCmd {
2960 #[command(subcommand)]
2961 pub command: RunnersAgentSubCommand,
2962}
2963
2964#[derive(Subcommand, Debug)]
2965pub enum RunnersAgentSubCommand {
2966 #[command(about = "Serve as a local runner host agent")]
2967 Serve(RunnersAgentServeCmd),
2968}
2969
2970#[derive(Args, Debug)]
2971pub struct RunnersAgentServeCmd {
2972 #[arg(long, help = "Runner host ID")]
2973 pub runner_host_id: String,
2974 #[arg(long, default_value_t = 15, help = "Polling interval in seconds")]
2975 pub interval_seconds: u64,
2976 #[arg(long, help = "Process at most one job and exit")]
2977 pub once: bool,
2978 #[command(flatten)]
2979 pub target: ApiTargetOptions,
2980}
2981
2982#[derive(Args, Debug)]
2983#[command(
2984 arg_required_else_help = true,
2985 disable_help_subcommand = true,
2986 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2987 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"
2988)]
2989pub struct McpCmd {
2990 #[command(subcommand)]
2991 pub command: McpSubCommand,
2992}
2993
2994#[derive(Subcommand, Debug)]
2995pub enum McpSubCommand {
2996 #[command(about = "Start the MCP HTTP/SSE server (foreground or detached)")]
2997 Serve(McpServeCmd),
2998 #[command(about = "Install and enable xbp-mcp.service via systemd")]
2999 Install(McpInstallCmd),
3000 #[command(about = "Check whether the MCP server is reachable")]
3001 Status(McpStatusCmd),
3002 #[command(about = "Print Cursor MCP configuration JSON")]
3003 Config(McpConfigCmd),
3004 #[command(about = "Print prefilled MCP Inspector URLs and tool-call presets")]
3005 Inspector(McpInspectorCmd),
3006 #[command(
3007 about = "Export the full clap-derived MCP tool catalog as JSON (regenerate crates/mcp/generated/catalog.json)"
3008 )]
3009 ExportTools(McpExportToolsCmd),
3010}
3011
3012#[derive(Args, Debug)]
3013pub struct McpExportToolsCmd {
3014 #[arg(
3015 long,
3016 short = 'o',
3017 help = "Write catalog JSON to this path (default: stdout)"
3018 )]
3019 pub output: Option<PathBuf>,
3020}
3021
3022#[derive(Args, Debug)]
3023pub struct McpServeCmd {
3024 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
3025 pub bind: String,
3026 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
3027 pub port: u16,
3028 #[arg(
3029 long,
3030 help = "Spawn a background MCP server process and print Cursor config"
3031 )]
3032 pub detach: bool,
3033 #[arg(long, help = "Use stdio MCP transport instead of HTTP/SSE")]
3034 pub stdio: bool,
3035 #[arg(
3036 long,
3037 help = "Show a system tray icon (xbp.app icon) with a Quit action"
3038 )]
3039 pub tray: bool,
3040}
3041
3042#[derive(Args, Debug)]
3043pub struct McpInstallCmd {
3044 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
3045 pub bind: String,
3046 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
3047 pub port: u16,
3048}
3049
3050#[derive(Args, Debug)]
3051pub struct McpStatusCmd {
3052 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
3053 pub bind: String,
3054 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
3055 pub port: u16,
3056}
3057
3058#[derive(Args, Debug)]
3059pub struct McpConfigCmd {
3060 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
3061 pub bind: String,
3062 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
3063 pub port: u16,
3064}
3065
3066#[derive(Args, Debug)]
3067pub struct McpInspectorCmd {
3068 #[arg(
3069 long,
3070 default_value = "127.0.0.1",
3071 help = "Bind address for the xbp MCP server"
3072 )]
3073 pub bind: String,
3074 #[arg(
3075 long,
3076 default_value_t = 1113,
3077 help = "HTTP port for the xbp MCP SSE transport"
3078 )]
3079 pub port: u16,
3080 #[arg(long, default_value_t = 6274, help = "MCP Inspector UI port")]
3081 pub inspector_port: u16,
3082 #[arg(
3083 long,
3084 help = "Write .xbp/mcp-inspector.json for `npx @modelcontextprotocol/inspector --config`"
3085 )]
3086 pub write_config: bool,
3087 #[arg(
3088 long,
3089 help = "Launch MCP Inspector via npx (writes config first; requires Node.js)"
3090 )]
3091 pub launch: bool,
3092 #[arg(long, help = "Emit machine-readable JSON playbook")]
3093 pub json: bool,
3094}
3095
3096#[derive(Args, Debug)]
3097#[command(
3098 arg_required_else_help = true,
3099 disable_help_subcommand = true,
3100 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3101 after_help = crate::cli::help_render::WORKTREE_WATCH_AFTER_HELP
3102)]
3103pub struct WorktreeWatchCmd {
3104 #[command(subcommand)]
3105 pub command: WorktreeWatchSubCommand,
3106}
3107
3108#[derive(Subcommand, Debug)]
3109pub enum WorktreeWatchSubCommand {
3110 #[command(about = "Watch the current git worktree and append mutation events locally")]
3111 Start(WorktreeWatchStartCmd),
3112 #[command(about = "Stop a detached background worktree watcher")]
3113 Stop(WorktreeWatchStopCmd),
3114 #[command(about = "Upload unsynced local mutation spool files to xbp.app")]
3115 Sync(WorktreeWatchSyncCmd),
3116 #[command(
3117 about = "Show spool path, sync backlog, and local activity stats",
3118 after_help = crate::cli::help_render::WORKTREE_WATCH_STATUS_AFTER_HELP
3119 )]
3120 Status(WorktreeWatchStatusCmd),
3121 #[command(
3122 about = "Open a native system tray UI to start/stop watchers and view stats (Windows + Linux/GNOME)",
3123 after_help = crate::cli::help_render::WORKTREE_WATCH_TRAY_AFTER_HELP
3124 )]
3125 Tray(WorktreeWatchTrayCmd),
3126}
3127
3128#[derive(Args, Debug, Clone)]
3129pub struct WorktreeWatchTarget {
3130 #[arg(
3131 long,
3132 conflicts_with_all = ["parent", "repos"],
3133 help = "Git repository root, short name, or owner/repo from system inventory; defaults to the current repo"
3134 )]
3135 pub repo: Option<PathBuf>,
3136 #[arg(
3137 long,
3138 conflicts_with_all = ["repo", "repos"],
3139 help = "Folder containing git repositories to target recursively"
3140 )]
3141 pub parent: Option<PathBuf>,
3142 #[arg(
3143 long = "repos",
3144 value_name = "PATH_OR_NAME",
3145 num_args = 1..,
3146 conflicts_with_all = ["repo", "parent"],
3147 help = "One or more git roots, short names, or owner/repo values from system inventory (space-separated)"
3148 )]
3149 pub repos: Vec<PathBuf>,
3150}
3151
3152#[derive(Args, Debug)]
3153pub struct WorktreeWatchTrayCmd {
3154 #[command(flatten)]
3155 pub target: WorktreeWatchTarget,
3156 #[arg(
3157 long,
3158 help = "Keep the tray attached to the terminal instead of backgrounding"
3159 )]
3160 pub foreground: bool,
3161 #[arg(
3162 long,
3163 default_value_t = 60,
3164 help = "Upload interval for watchers started from the tray (seconds; 0 disables)"
3165 )]
3166 pub sync_interval_seconds: u64,
3167}
3168
3169#[derive(Args, Debug)]
3170pub struct WorktreeWatchStartCmd {
3171 #[command(flatten)]
3172 pub target: WorktreeWatchTarget,
3173 #[arg(long, help = "Spawn the watcher as a detached background process")]
3174 pub detach: bool,
3175 #[arg(
3176 long,
3177 default_value_t = 60,
3178 help = "Upload queued events every N seconds while watching; set 0 to disable periodic sync"
3179 )]
3180 pub sync_interval_seconds: u64,
3181 #[arg(long, help = "Record current git commit state once and exit")]
3182 pub once: bool,
3183}
3184
3185#[derive(Args, Debug)]
3186pub struct WorktreeWatchStopCmd {
3187 #[command(flatten)]
3188 pub target: WorktreeWatchTarget,
3189 #[arg(
3190 long,
3191 help = "Stop the PID from watcher state even when the command line cannot be verified"
3192 )]
3193 pub force: bool,
3194}
3195
3196#[derive(Args, Debug)]
3197pub struct WorktreeWatchSyncCmd {
3198 #[command(flatten)]
3199 pub target: WorktreeWatchTarget,
3200 #[arg(long, help = "Print the upload plan without sending it to xbp.app")]
3201 pub dry_run: bool,
3202 #[arg(
3203 long,
3204 help = "Upload all local JSONL spool files, including files already marked synced"
3205 )]
3206 pub resync: bool,
3207}
3208
3209#[derive(Args, Debug)]
3210pub struct WorktreeWatchStatusCmd {
3211 #[command(flatten)]
3212 pub target: WorktreeWatchTarget,
3213 #[arg(long, help = "Emit status as JSON")]
3214 pub json: bool,
3215 #[arg(
3216 long,
3217 help = "Print decoded mutation and commit records from local JSONL spool files"
3218 )]
3219 pub records: bool,
3220 #[arg(
3221 long,
3222 default_value_t = 50,
3223 requires = "records",
3224 help = "Maximum decoded JSONL records to print per repository"
3225 )]
3226 pub record_limit: usize,
3227 #[arg(
3228 long,
3229 help = "Generate and print activity statistics from local JSONL spool files (writes stats.json)"
3230 )]
3231 pub stats: bool,
3232 #[arg(
3233 long,
3234 help = "Generate and print repository activity across all locally spooled branches (writes repo-activity.json)"
3235 )]
3236 pub repo_activity: bool,
3237 #[arg(
3238 long,
3239 default_value_t = 15,
3240 help = "Maximum minutes between mutation events counted as one coding session (used by --stats / --repo-activity)"
3241 )]
3242 pub stats_gap_minutes: u64,
3243}
3244
3245#[derive(Args, Debug, Clone, Default)]
3246pub struct ApiTargetOptions {
3247 #[arg(long, help = "Override the request base URL for this command")]
3248 pub base_url: Option<String>,
3249 #[arg(
3250 long,
3251 help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
3252 )]
3253 pub web: bool,
3254 #[arg(
3255 long,
3256 help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
3257 )]
3258 pub no_auth: bool,
3259 #[arg(
3260 long,
3261 help = "Extra header in 'Name: Value' format",
3262 value_name = "HEADER"
3263 )]
3264 pub header: Vec<String>,
3265 #[arg(long, help = "Print response headers")]
3266 pub include_headers: bool,
3267 #[arg(
3268 long,
3269 help = "Print the response body as-is without JSON pretty formatting"
3270 )]
3271 pub raw: bool,
3272}
3273
3274#[cfg(feature = "docker")]
3275#[derive(Args, Debug)]
3276pub struct DockerCmd {
3277 #[arg(
3278 trailing_var_arg = true,
3279 allow_hyphen_values = true,
3280 help = "Arguments to pass directly to the Docker CLI (default: --help)"
3281 )]
3282 pub args: Vec<String>,
3283}
3284
3285#[derive(Subcommand, Debug)]
3286pub enum ApiSubCommand {
3287 #[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
3288 Install {
3289 #[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
3290 port: u16,
3291 },
3292 #[command(about = "Call the XBP API health endpoint")]
3293 Health(ApiHealthCmd),
3294 #[command(about = "Manage XBP control-plane projects")]
3295 Projects(ApiProjectsCmd),
3296 #[command(about = "Manage XBP daemon registrations and heartbeats")]
3297 Daemons(ApiDaemonsCmd),
3298 #[command(about = "Manage XBP deployment jobs")]
3299 Jobs(ApiJobsCmd),
3300 #[command(about = "Manage XBP runner hosts, groups, inventory, and runner jobs")]
3301 Runners(ApiRunnersCmd),
3302 #[command(about = "Manage XBP proxy routes on the local API server")]
3303 Routes(ApiRoutesCmd),
3304 #[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
3305 Request(ApiRequestCmd),
3306}
3307
3308#[derive(Args, Debug)]
3309pub struct ApiHealthCmd {
3310 #[command(flatten)]
3311 pub target: ApiTargetOptions,
3312}
3313
3314#[derive(Args, Debug)]
3315pub struct ApiProjectsCmd {
3316 #[command(subcommand)]
3317 pub command: ApiProjectsSubCommand,
3318}
3319
3320#[derive(Subcommand, Debug)]
3321pub enum ApiProjectsSubCommand {
3322 #[command(about = "List projects from the XBP control-plane API")]
3323 List(ApiProjectsListCmd),
3324 #[command(about = "Create or upsert a control-plane project")]
3325 Create(Box<ApiProjectsCreateCmd>),
3326}
3327
3328#[derive(Args, Debug)]
3329pub struct ApiProjectsListCmd {
3330 #[arg(long, help = "Optional organization ID filter")]
3331 pub organization_id: Option<String>,
3332 #[command(flatten)]
3333 pub target: ApiTargetOptions,
3334}
3335
3336#[derive(Args, Debug)]
3337pub struct ApiProjectsCreateCmd {
3338 #[arg(long, help = "Project name")]
3339 pub name: String,
3340 #[arg(long, help = "Project path or repo path key")]
3341 pub path: String,
3342 #[arg(long, help = "Optional organization ID")]
3343 pub organization_id: Option<String>,
3344 #[arg(long, help = "Optional project slug")]
3345 pub slug: Option<String>,
3346 #[arg(long, help = "Optional project version")]
3347 pub version: Option<String>,
3348 #[arg(long, help = "Optional build directory")]
3349 pub build_dir: Option<String>,
3350 #[arg(long, help = "Optional runtime enum value")]
3351 pub runtime: Option<String>,
3352 #[arg(long, help = "Optional default branch")]
3353 pub default_branch: Option<String>,
3354 #[arg(long, help = "Optional repository root directory")]
3355 pub root_directory: Option<String>,
3356 #[arg(long, help = "Optional build command")]
3357 pub build_command: Option<String>,
3358 #[arg(long, help = "Optional install command")]
3359 pub install_command: Option<String>,
3360 #[arg(long, help = "Optional start command")]
3361 pub start_command: Option<String>,
3362 #[arg(long, help = "Optional output directory")]
3363 pub output_directory: Option<String>,
3364 #[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
3365 pub repository_json: Option<String>,
3366 #[arg(long, help = "Runtime policy JSON payload")]
3367 pub runtime_policy_json: Option<String>,
3368 #[arg(long, help = "Metadata JSON object")]
3369 pub metadata_json: Option<String>,
3370 #[command(flatten)]
3371 pub target: ApiTargetOptions,
3372}
3373
3374#[derive(Args, Debug)]
3375pub struct ApiDaemonsCmd {
3376 #[command(subcommand)]
3377 pub command: ApiDaemonsSubCommand,
3378}
3379
3380#[derive(Subcommand, Debug)]
3381pub enum ApiDaemonsSubCommand {
3382 #[command(about = "List registered daemons")]
3383 List(ApiDaemonsListCmd),
3384 #[command(about = "Register or upsert a daemon record")]
3385 Register(ApiDaemonsRegisterCmd),
3386 #[command(about = "Post a heartbeat update for a daemon")]
3387 Heartbeat(ApiDaemonsHeartbeatCmd),
3388 #[command(about = "Update daemon status only")]
3389 UpdateStatus(ApiDaemonsUpdateStatusCmd),
3390}
3391
3392#[derive(Args, Debug)]
3393pub struct ApiDaemonsListCmd {
3394 #[command(flatten)]
3395 pub target: ApiTargetOptions,
3396}
3397
3398#[derive(Args, Debug)]
3399pub struct ApiDaemonsRegisterCmd {
3400 #[arg(long, help = "Daemon node name")]
3401 pub node_name: String,
3402 #[arg(long, help = "Daemon hostname")]
3403 pub hostname: String,
3404 #[arg(long, help = "Daemon binary version")]
3405 pub version: String,
3406 #[arg(long, help = "Optional region")]
3407 pub region: Option<String>,
3408 #[arg(long, help = "Optional public IP")]
3409 pub public_ip: Option<String>,
3410 #[arg(long, help = "Optional internal IP")]
3411 pub internal_ip: Option<String>,
3412 #[arg(long, help = "Optional status enum value")]
3413 pub status: Option<String>,
3414 #[arg(long, help = "Optional CPU core count")]
3415 pub cpu_cores: Option<i32>,
3416 #[arg(long, help = "Optional total memory in MB")]
3417 pub memory_total_mb: Option<i32>,
3418 #[arg(long, help = "Optional total disk in GB")]
3419 pub disk_total_gb: Option<i32>,
3420 #[arg(long, help = "Labels JSON object")]
3421 pub labels_json: Option<String>,
3422 #[arg(long, help = "Metadata JSON object")]
3423 pub metadata_json: Option<String>,
3424 #[command(flatten)]
3425 pub target: ApiTargetOptions,
3426}
3427
3428#[derive(Args, Debug)]
3429pub struct ApiDaemonsHeartbeatCmd {
3430 #[arg(help = "Daemon ID")]
3431 pub daemon_id: String,
3432 #[arg(long, help = "Optional status enum value")]
3433 pub status: Option<String>,
3434 #[arg(long, help = "Optional daemon version")]
3435 pub version: Option<String>,
3436 #[arg(long, help = "Optional public IP")]
3437 pub public_ip: Option<String>,
3438 #[arg(long, help = "Optional internal IP")]
3439 pub internal_ip: Option<String>,
3440 #[arg(long, help = "Optional CPU core count")]
3441 pub cpu_cores: Option<i32>,
3442 #[arg(long, help = "Optional total memory in MB")]
3443 pub memory_total_mb: Option<i32>,
3444 #[arg(long, help = "Optional total disk in GB")]
3445 pub disk_total_gb: Option<i32>,
3446 #[arg(long, help = "Labels JSON object")]
3447 pub labels_json: Option<String>,
3448 #[command(flatten)]
3449 pub target: ApiTargetOptions,
3450}
3451
3452#[derive(Args, Debug)]
3453pub struct ApiDaemonsUpdateStatusCmd {
3454 #[arg(help = "Daemon ID")]
3455 pub daemon_id: String,
3456 #[arg(long, help = "Daemon status enum value")]
3457 pub status: String,
3458 #[command(flatten)]
3459 pub target: ApiTargetOptions,
3460}
3461
3462#[derive(Args, Debug)]
3463pub struct ApiJobsCmd {
3464 #[command(subcommand)]
3465 pub command: ApiJobsSubCommand,
3466}
3467
3468#[derive(Args, Debug)]
3469pub struct ApiRunnersCmd {
3470 #[command(subcommand)]
3471 pub command: ApiRunnersSubCommand,
3472}
3473
3474#[derive(Subcommand, Debug)]
3475pub enum ApiRunnersSubCommand {
3476 #[command(about = "List runner hosts")]
3477 HostsList(ApiRunnersHostsListCmd),
3478 #[command(about = "Register or upsert a runner host")]
3479 HostsEnroll(ApiRunnersHostsEnrollCmd),
3480 #[command(about = "Post a heartbeat update for a runner host")]
3481 HostsHeartbeat(ApiRunnersHostsHeartbeatCmd),
3482 #[command(about = "List persisted preflight records for a runner host")]
3483 HostsPreflightsList(ApiRunnersHostsPreflightsListCmd),
3484 #[command(about = "Create or replace a persisted preflight record for a runner host")]
3485 HostsPreflightsUpsert(ApiRunnersHostsPreflightsUpsertCmd),
3486 #[command(about = "List runner groups for an organization")]
3487 GroupsList(ApiRunnersGroupsListCmd),
3488 #[command(about = "Create or upsert a runner group")]
3489 GroupsCreate(ApiRunnersGroupsCreateCmd),
3490 #[command(about = "Update an existing runner group")]
3491 GroupsUpdate(ApiRunnersGroupsCreateCmd),
3492 #[command(about = "Delete a runner group")]
3493 GroupsDelete(ApiRunnersGroupsDeleteCmd),
3494 #[command(about = "List repository access for a runner group")]
3495 GroupRepositoriesList(ApiRunnersGroupRepositoriesListCmd),
3496 #[command(about = "Grant or upsert repository access for a runner group")]
3497 GroupRepositoriesCreate(ApiRunnersGroupRepositoriesCreateCmd),
3498 #[command(about = "List runner inventory for an organization")]
3499 InventoryList(ApiRunnersInventoryListCmd),
3500 #[command(about = "Upsert a runner inventory record")]
3501 InventoryUpsert(ApiRunnersInventoryUpsertCmd),
3502 #[command(about = "List runner jobs")]
3503 JobsList(ApiRunnersJobsListCmd),
3504 #[command(about = "Create a runner job")]
3505 JobsCreate(ApiRunnersJobsCreateCmd),
3506 #[command(about = "Claim the next runner job")]
3507 JobsClaim(ApiRunnersJobsClaimCmd),
3508 #[command(about = "Update a runner job")]
3509 JobsUpdate(ApiRunnersJobsUpdateCmd),
3510}
3511
3512#[derive(Args, Debug)]
3513pub struct ApiRunnersHostsListCmd {
3514 #[arg(long)]
3515 pub organization_id: Option<String>,
3516 #[arg(long)]
3517 pub daemon_id: Option<String>,
3518 #[arg(long)]
3519 pub status: Option<String>,
3520 #[command(flatten)]
3521 pub target: ApiTargetOptions,
3522}
3523
3524#[derive(Args, Debug)]
3525pub struct ApiRunnersHostsEnrollCmd {
3526 #[arg(long)]
3527 pub organization_id: String,
3528 #[arg(long)]
3529 pub daemon_id: Option<String>,
3530 #[arg(long)]
3531 pub host_kind: String,
3532 #[arg(long)]
3533 pub platform: String,
3534 #[arg(long)]
3535 pub hostname: String,
3536 #[arg(long)]
3537 pub runner_name_prefix: String,
3538 #[arg(long)]
3539 pub display_name: Option<String>,
3540 #[arg(long)]
3541 pub arch: Option<String>,
3542 #[arg(long)]
3543 pub status: Option<String>,
3544 #[arg(long)]
3545 pub labels_json: Option<String>,
3546 #[arg(long)]
3547 pub capabilities_json: Option<String>,
3548 #[arg(long)]
3549 pub metadata_json: Option<String>,
3550 #[command(flatten)]
3551 pub target: ApiTargetOptions,
3552}
3553
3554#[derive(Args, Debug)]
3555pub struct ApiRunnersHostsHeartbeatCmd {
3556 #[arg(help = "Runner host ID")]
3557 pub runner_host_id: String,
3558 #[arg(long)]
3559 pub status: Option<String>,
3560 #[arg(long)]
3561 pub labels_json: Option<String>,
3562 #[arg(long)]
3563 pub capabilities_json: Option<String>,
3564 #[command(flatten)]
3565 pub target: ApiTargetOptions,
3566}
3567
3568#[derive(Args, Debug)]
3569pub struct ApiRunnersHostsPreflightsListCmd {
3570 #[arg(long)]
3571 pub runner_host_id: String,
3572 #[command(flatten)]
3573 pub target: ApiTargetOptions,
3574}
3575
3576#[derive(Args, Debug)]
3577pub struct ApiRunnersHostsPreflightsUpsertCmd {
3578 #[arg(long)]
3579 pub runner_host_id: String,
3580 #[arg(long)]
3581 pub platform: String,
3582 #[arg(long)]
3583 pub status: String,
3584 #[arg(long)]
3585 pub docker_available: Option<bool>,
3586 #[arg(long)]
3587 pub service_manager: Option<String>,
3588 #[arg(long)]
3589 pub checks_json: Option<String>,
3590 #[arg(long)]
3591 pub checked_at: Option<String>,
3592 #[command(flatten)]
3593 pub target: ApiTargetOptions,
3594}
3595
3596#[derive(Args, Debug, Clone)]
3597pub struct ApiRunnersGroupsListCmd {
3598 #[arg(long)]
3599 pub organization_id: String,
3600 #[command(flatten)]
3601 pub target: ApiTargetOptions,
3602}
3603
3604#[derive(Args, Debug, Clone)]
3605pub struct ApiRunnersGroupsCreateCmd {
3606 #[arg(long)]
3607 pub organization_id: String,
3608 #[arg(long)]
3609 pub name: String,
3610 #[arg(long)]
3611 pub visibility: Option<String>,
3612 #[arg(long)]
3613 pub github_installation_id: Option<String>,
3614 #[arg(long)]
3615 pub github_runner_group_id: Option<i64>,
3616 #[arg(long)]
3617 pub sync_state: Option<String>,
3618 #[arg(long)]
3619 pub sync_error: Option<String>,
3620 #[arg(long)]
3621 pub inherited: bool,
3622 #[arg(long)]
3623 pub allows_public_repositories: bool,
3624 #[arg(long)]
3625 pub interactive: bool,
3626 #[arg(long)]
3627 pub restricted_to_workflows: bool,
3628 #[arg(long)]
3629 pub selected_workflows_json: Option<String>,
3630 #[arg(long)]
3631 pub metadata_json: Option<String>,
3632 #[command(flatten)]
3633 pub target: ApiTargetOptions,
3634}
3635
3636#[derive(Args, Debug)]
3637pub struct ApiRunnersGroupsDeleteCmd {
3638 #[arg(help = "Runner group ID")]
3639 pub runner_group_id: String,
3640 #[command(flatten)]
3641 pub target: ApiTargetOptions,
3642}
3643
3644#[derive(Args, Debug)]
3645pub struct ApiRunnersGroupRepositoriesListCmd {
3646 #[arg(help = "Runner group ID")]
3647 pub runner_group_id: String,
3648 #[command(flatten)]
3649 pub target: ApiTargetOptions,
3650}
3651
3652#[derive(Args, Debug)]
3653pub struct ApiRunnersGroupRepositoriesCreateCmd {
3654 #[arg(help = "Runner group ID")]
3655 pub runner_group_id: String,
3656 #[arg(long)]
3657 pub github_repository_id: Option<i64>,
3658 #[arg(long)]
3659 pub repository_owner: String,
3660 #[arg(long)]
3661 pub repository_name: String,
3662 #[arg(long)]
3663 pub repository_full_name: String,
3664 #[arg(long)]
3665 pub is_private: bool,
3666 #[command(flatten)]
3667 pub target: ApiTargetOptions,
3668}
3669
3670#[derive(Args, Debug)]
3671pub struct ApiRunnersInventoryListCmd {
3672 #[arg(long)]
3673 pub organization_id: String,
3674 #[command(flatten)]
3675 pub target: ApiTargetOptions,
3676}
3677
3678#[derive(Args, Debug)]
3679pub struct ApiRunnersInventoryUpsertCmd {
3680 #[arg(long)]
3681 pub organization_id: String,
3682 #[arg(long)]
3683 pub runner_host_id: Option<String>,
3684 #[arg(long)]
3685 pub runner_group_id: Option<String>,
3686 #[arg(long)]
3687 pub github_runner_id: Option<i64>,
3688 #[arg(long)]
3689 pub name: String,
3690 #[arg(long)]
3691 pub platform: String,
3692 #[arg(long)]
3693 pub os: Option<String>,
3694 #[arg(long)]
3695 pub architecture: Option<String>,
3696 #[arg(long)]
3697 pub status: Option<String>,
3698 #[arg(long)]
3699 pub busy: bool,
3700 #[arg(long)]
3701 pub labels_json: Option<String>,
3702 #[arg(long)]
3703 pub metadata_json: Option<String>,
3704 #[command(flatten)]
3705 pub target: ApiTargetOptions,
3706}
3707
3708#[derive(Args, Debug, Clone)]
3709pub struct ApiRunnersJobsListCmd {
3710 #[arg(long)]
3711 pub organization_id: Option<String>,
3712 #[arg(long)]
3713 pub runner_host_id: Option<String>,
3714 #[arg(long)]
3715 pub runner_id: Option<String>,
3716 #[arg(long)]
3717 pub daemon_id: Option<String>,
3718 #[arg(long)]
3719 pub status: Option<String>,
3720 #[arg(long)]
3721 pub phase: Option<String>,
3722 #[arg(long)]
3723 pub limit: Option<usize>,
3724 #[command(flatten)]
3725 pub target: ApiTargetOptions,
3726}
3727
3728#[derive(Args, Debug)]
3729pub struct ApiRunnersJobsCreateCmd {
3730 #[arg(long)]
3731 pub organization_id: String,
3732 #[arg(long)]
3733 pub runner_host_id: Option<String>,
3734 #[arg(long)]
3735 pub daemon_id: Option<String>,
3736 #[arg(long)]
3737 pub runner_id: Option<String>,
3738 #[arg(long)]
3739 pub job_kind: String,
3740 #[arg(long)]
3741 pub priority: Option<i32>,
3742 #[arg(long)]
3743 pub max_attempts: Option<i32>,
3744 #[arg(long)]
3745 pub run_after: Option<String>,
3746 #[arg(long)]
3747 pub payload_json: Option<String>,
3748 #[command(flatten)]
3749 pub target: ApiTargetOptions,
3750}
3751
3752#[derive(Args, Debug)]
3753pub struct ApiRunnersJobsClaimCmd {
3754 #[arg(long)]
3755 pub runner_host_id: Option<String>,
3756 #[arg(long)]
3757 pub daemon_id: Option<String>,
3758 #[arg(long)]
3759 pub locked_by: Option<String>,
3760 #[command(flatten)]
3761 pub target: ApiTargetOptions,
3762}
3763
3764#[derive(Args, Debug)]
3765pub struct ApiRunnersJobsUpdateCmd {
3766 #[arg(help = "Runner job ID")]
3767 pub runner_job_id: String,
3768 #[arg(long)]
3769 pub status: String,
3770 #[arg(long)]
3771 pub error_text: Option<String>,
3772 #[command(flatten)]
3773 pub target: ApiTargetOptions,
3774}
3775
3776#[derive(Subcommand, Debug)]
3777pub enum ApiJobsSubCommand {
3778 #[command(about = "List deployment jobs")]
3779 List(ApiJobsListCmd),
3780 #[command(about = "Create a deployment job for a project")]
3781 Create(ApiJobsCreateCmd),
3782 #[command(about = "Claim the next deployment job for a daemon")]
3783 Claim(ApiJobsClaimCmd),
3784 #[command(about = "Update deployment job status")]
3785 Update(ApiJobsUpdateCmd),
3786}
3787
3788#[derive(Args, Debug)]
3789pub struct ApiJobsListCmd {
3790 #[arg(long, help = "Optional project ID filter")]
3791 pub project_id: Option<String>,
3792 #[arg(long, help = "Optional deployment ID filter")]
3793 pub deployment_id: Option<String>,
3794 #[arg(long, help = "Optional daemon ID filter")]
3795 pub daemon_id: Option<String>,
3796 #[arg(long, help = "Optional status filter")]
3797 pub status: Option<String>,
3798 #[arg(long, help = "Optional result limit")]
3799 pub limit: Option<usize>,
3800 #[command(flatten)]
3801 pub target: ApiTargetOptions,
3802}
3803
3804#[derive(Args, Debug)]
3805pub struct ApiJobsCreateCmd {
3806 #[arg(long, help = "Project ID")]
3807 pub project_id: String,
3808 #[arg(long, help = "Deployment ID")]
3809 pub deployment_id: String,
3810 #[arg(long, help = "Optional daemon ID assignment")]
3811 pub daemon_id: Option<String>,
3812 #[arg(long, help = "Optional priority")]
3813 pub priority: Option<i32>,
3814 #[arg(long, help = "Optional max attempts")]
3815 pub max_attempts: Option<i32>,
3816 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
3817 pub run_after: Option<String>,
3818 #[arg(long, help = "Optional payload JSON object")]
3819 pub payload_json: Option<String>,
3820 #[command(flatten)]
3821 pub target: ApiTargetOptions,
3822}
3823
3824#[derive(Args, Debug)]
3825pub struct ApiJobsClaimCmd {
3826 #[arg(long, help = "Daemon ID claiming work")]
3827 pub daemon_id: String,
3828 #[arg(long, help = "Optional lock owner")]
3829 pub locked_by: Option<String>,
3830 #[command(flatten)]
3831 pub target: ApiTargetOptions,
3832}
3833
3834#[derive(Args, Debug)]
3835pub struct ApiJobsUpdateCmd {
3836 #[arg(help = "Deployment job ID")]
3837 pub job_id: String,
3838 #[arg(long, help = "Deployment job status enum value")]
3839 pub status: String,
3840 #[arg(long, help = "Optional error text")]
3841 pub error_text: Option<String>,
3842 #[command(flatten)]
3843 pub target: ApiTargetOptions,
3844}
3845
3846#[derive(Args, Debug)]
3847pub struct ApiRoutesCmd {
3848 #[command(subcommand)]
3849 pub command: ApiRoutesSubCommand,
3850}
3851
3852#[derive(Subcommand, Debug)]
3853pub enum ApiRoutesSubCommand {
3854 #[command(about = "List configured proxy routes")]
3855 List(ApiRoutesListCmd),
3856 #[command(about = "Create or replace a proxy route")]
3857 Create(ApiRoutesCreateCmd),
3858 #[command(about = "Delete a proxy route by domain")]
3859 Delete(ApiRoutesDeleteCmd),
3860}
3861
3862#[derive(Args, Debug)]
3863pub struct ApiRoutesListCmd {
3864 #[command(flatten)]
3865 pub target: ApiTargetOptions,
3866}
3867
3868#[derive(Args, Debug)]
3869pub struct ApiRoutesCreateCmd {
3870 #[arg(long, help = "Domain name for the route")]
3871 pub domain: String,
3872 #[arg(long, help = "Upstream target URL", required = true)]
3873 pub target: Vec<String>,
3874 #[arg(
3875 long,
3876 help = "Weighted upstream target in url=weight form",
3877 value_name = "URL=WEIGHT"
3878 )]
3879 pub weighted_target: Vec<String>,
3880 #[arg(long, help = "Optional header condition")]
3881 pub header_condition: Option<String>,
3882 #[arg(long, help = "Optional path prefix condition")]
3883 pub path_prefix: Option<String>,
3884 #[command(flatten)]
3885 pub target_options: ApiTargetOptions,
3886}
3887
3888#[derive(Args, Debug)]
3889pub struct ApiRoutesDeleteCmd {
3890 #[arg(help = "Domain name for the route")]
3891 pub domain: String,
3892 #[command(flatten)]
3893 pub target: ApiTargetOptions,
3894}
3895
3896#[derive(Args, Debug)]
3897pub struct ApiRequestCmd {
3898 #[arg(help = "Request path like /projects or a full https:// URL")]
3899 pub path: String,
3900 #[arg(
3901 short = 'X',
3902 long,
3903 help = "HTTP method to use (default: GET, or POST when a body is provided)"
3904 )]
3905 pub method: Option<String>,
3906 #[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
3907 pub body: Option<String>,
3908 #[arg(long, help = "Read the request body from a file")]
3909 pub body_file: Option<PathBuf>,
3910 #[command(flatten)]
3911 pub target: ApiTargetOptions,
3912}
3913#[derive(Args, Debug)]
3914pub struct TailCmd {
3915 #[arg(long, help = "Tail Kafka topic instead of log files")]
3916 pub kafka: bool,
3917 #[arg(long, help = "Ship logs to Kafka")]
3918 pub ship: bool,
3919}
3920
3921#[derive(Args, Debug)]
3922#[command(
3923 arg_required_else_help = true,
3924 disable_help_subcommand = true,
3925 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3926 after_help = crate::cli::help_render::GENERATE_AFTER_HELP
3927)]
3928pub struct GenerateCmd {
3929 #[command(subcommand)]
3930 pub command: GenerateSubCommand,
3931}
3932
3933#[derive(Subcommand, Debug)]
3934pub enum GenerateSubCommand {
3935 #[command(about = "Generate or update .xbp/xbp.toml (and convert legacy JSON)")]
3936 Config(GenerateConfigCmd),
3937 #[cfg(feature = "openapi-gen")]
3938 #[command(about = "Generate static OpenAPI documents for configured services")]
3939 Openapi(GenerateOpenApiCmd),
3940 Systemd(GenerateSystemdCmd),
3941}
3942
3943#[cfg(feature = "openapi-gen")]
3944#[derive(Args, Debug)]
3945pub struct GenerateOpenApiCmd {
3946 #[arg(long, action = clap::ArgAction::Append, help = "Generate only the named service (repeatable)")]
3947 pub service: Vec<String>,
3948 #[arg(
3949 long,
3950 conflicts_with = "service",
3951 help = "Generate every eligible service and the aggregate"
3952 )]
3953 pub all: bool,
3954 #[arg(long, value_parser = ["yaml", "json", "both"], help = "Override configured output formats")]
3955 pub format: Option<String>,
3956 #[arg(
3957 long,
3958 help = "Override the output path (requires one service and one format)"
3959 )]
3960 pub output: Option<PathBuf>,
3961 #[arg(long, help = "Fail if generated output differs without writing files")]
3962 pub check: bool,
3963 #[arg(long, help = "Render and validate without writing files")]
3964 pub dry_run: bool,
3965 #[arg(long, help = "Fail when a source type cannot be resolved")]
3966 pub strict: bool,
3967 #[arg(long, help = "Ignore the parsed-source cache")]
3968 pub no_cache: bool,
3969}
3970
3971#[derive(Args, Debug)]
3972pub struct GenerateConfigCmd {
3973 #[arg(
3974 long,
3975 help = "Overwrite .xbp/xbp.toml if it already exists (default errors when present)"
3976 )]
3977 pub force: bool,
3978 #[arg(
3979 long,
3980 help = "Refresh .xbp/xbp.toml by applying project detection defaults for missing fields"
3981 )]
3982 pub update: bool,
3983 #[arg(
3984 long,
3985 help = "Path to a legacy xbp.json file to convert into .xbp/xbp.toml"
3986 )]
3987 pub from_json: Option<PathBuf>,
3988}
3989
3990#[derive(Args, Debug)]
3991#[command(
3992 arg_required_else_help = true,
3993 disable_help_subcommand = true,
3994 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3995 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"
3996)]
3997pub struct WorkersCmd {
3998 #[arg(
3999 long,
4000 help = "Workers project root (defaults to current dir, or apps/web inside the current XBP repo when present)"
4001 )]
4002 pub root: Option<PathBuf>,
4003 #[arg(
4004 long,
4005 help = "Worker app name from .xbp/xbp.toml `workers:` (defaults to the app matching the current directory)"
4006 )]
4007 pub app: Option<String>,
4008 #[arg(
4009 long,
4010 help = "Operate on every known Worker app when the subcommand supports multi-app selection (ignored by repo-scoped helpers such as `worktree paths`)"
4011 )]
4012 pub all: bool,
4013 #[arg(long, help = "Cloudflare API token override")]
4014 pub token: Option<String>,
4015 #[arg(long, help = "Cloudflare account ID override")]
4016 pub account_id: Option<String>,
4017 #[command(subcommand)]
4018 pub command: WorkersSubCommand,
4019}
4020
4021#[derive(Subcommand, Debug)]
4022pub enum WorkersSubCommand {
4023 #[command(
4024 alias = "ls",
4025 about = "List Cloudflare Worker scripts with build and placement status",
4026 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"
4027 )]
4028 List(WorkersListCmd),
4029 #[command(
4030 about = "Show deployment status, stream runtime logs, or fetch Workers Builds CI logs",
4031 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"
4032 )]
4033 Logs(WorkersLogsCmd),
4034 #[command(about = "Manage secret bindings for a Worker script or Wrangler environment")]
4035 Secrets(WorkersSecretsCmd),
4036 #[command(about = "Fetch remote Worker settings via the Cloudflare API")]
4037 Settings(WorkersSettingsCmd),
4038 #[command(about = "Inspect and synchronize Durable Object namespace configuration")]
4039 DurableObjects(WorkersDurableObjectsCmd),
4040 #[command(about = "Inspect or generate Wrangler config helpers")]
4041 Wrangler(WorkersWranglerCmd),
4042 #[command(about = "Run Wrangler D1 migration helpers")]
4043 D1(WorkersD1Cmd),
4044 #[command(about = "Run Worker deploy and predeploy helpers")]
4045 Deploy(WorkersDeployCmd),
4046 #[command(about = "Inspect shared worktree paths or link shared dev files")]
4047 Worktree(WorkersWorktreeCmd),
4048 #[command(about = "Show resolved Worker runtime metadata from local env and config files")]
4049 Env(WorkersEnvCmd),
4050}
4051
4052#[derive(Args, Debug, Default)]
4053pub struct WorkersListCmd {
4054 #[arg(
4055 long,
4056 help = "Show every Worker in the account instead of only Workers for this XBP project"
4057 )]
4058 pub all: bool,
4059 #[arg(long, help = "Output the worker inventory as JSON")]
4060 pub json: bool,
4061 #[arg(short = 'n', long = "limit", help = "Show at most N workers")]
4062 pub limit: Option<usize>,
4063 #[arg(
4064 long = "status",
4065 help = "Filter by build status (failed, success, running, unknown)"
4066 )]
4067 pub status: Option<String>,
4068 #[arg(long, help = "Shortcut for --status failed")]
4069 pub failed: bool,
4070 #[arg(
4071 long = "sort",
4072 default_value = "name",
4073 help = "Sort workers by name, modified, or status"
4074 )]
4075 pub sort: String,
4076 #[arg(long = "no-color", help = "Disable ANSI colors in table output")]
4077 pub no_color: bool,
4078}
4079
4080#[derive(Args, Debug)]
4081pub struct WorkersLogsCmd {
4082 #[command(flatten)]
4083 pub target: WorkersTargetArgs,
4084 #[arg(
4085 short = 'f',
4086 long = "follow",
4087 help = "Stream runtime logs until interrupted (wrangler tail)"
4088 )]
4089 pub follow: bool,
4090 #[arg(
4091 long = "build",
4092 help = "Show Workers Builds CI logs instead of runtime deployment output"
4093 )]
4094 pub build: bool,
4095 #[arg(
4096 long = "failed",
4097 help = "When using --build, prefer the latest failed build"
4098 )]
4099 pub failed: bool,
4100 #[arg(long, help = "Output logs as JSON")]
4101 pub json: bool,
4102 #[arg(
4103 short = 'n',
4104 long = "lines",
4105 help = "Show only the last N log lines (deployments/build output)"
4106 )]
4107 pub lines: Option<usize>,
4108 #[arg(
4109 short = 'o',
4110 long = "output",
4111 help = "Write log output to a file instead of stdout"
4112 )]
4113 pub output: Option<PathBuf>,
4114 #[arg(
4115 short = 'g',
4116 long = "grep",
4117 help = "Filter log lines matching this pattern (case-insensitive substring)"
4118 )]
4119 pub grep: Option<String>,
4120 #[arg(
4121 short = 'e',
4122 long = "errors-only",
4123 help = "Show only lines that look like errors or failures"
4124 )]
4125 pub errors_only: bool,
4126 #[arg(long = "no-color", help = "Disable ANSI colors in log output")]
4127 pub no_color: bool,
4128 #[arg(
4129 long = "list-builds",
4130 help = "List recent Workers Builds before fetching logs (with --build)"
4131 )]
4132 pub list_builds: bool,
4133 #[arg(
4134 long = "build-index",
4135 help = "Select build by index from --list-builds (0 = latest)"
4136 )]
4137 pub build_index: Option<usize>,
4138 #[arg(
4139 long,
4140 help = "Poll until the selected Workers Build finishes before fetching logs"
4141 )]
4142 pub wait: bool,
4143 #[arg(long = "no-wait", help = "Skip polling for in-progress Workers Builds")]
4144 pub no_wait: bool,
4145 #[arg(
4146 long = "wait-seconds",
4147 default_value_t = 120,
4148 help = "Maximum seconds to poll an in-progress Workers Build"
4149 )]
4150 pub wait_seconds: u64,
4151 #[arg(help = "Worker script name. When omitted, xbp prompts interactively.")]
4152 pub script_name: Option<String>,
4153}
4154
4155#[derive(Args, Debug, Clone, Default)]
4156pub struct WorkersTargetArgs {
4157 #[arg(
4158 long,
4159 help = "Worker base name (defaults to wrangler config name or xbp)"
4160 )]
4161 pub worker: Option<String>,
4162 #[arg(
4163 long = "environment",
4164 alias = "env",
4165 help = "Wrangler environment name. The remote script resolves to <worker>-<environment>."
4166 )]
4167 pub environment: Option<String>,
4168 #[arg(
4169 long,
4170 help = "Exact remote script name override. When set, this bypasses <worker>-<environment> resolution."
4171 )]
4172 pub script: Option<String>,
4173}
4174
4175#[derive(Args, Debug)]
4176pub struct WorkersSecretsCmd {
4177 #[command(flatten)]
4178 pub target: WorkersTargetArgs,
4179 #[command(subcommand)]
4180 pub command: WorkersSecretsSubCommand,
4181}
4182
4183#[derive(Subcommand, Debug)]
4184pub enum WorkersSecretsSubCommand {
4185 #[command(about = "List secret bindings on the resolved Worker script")]
4186 List(WorkersSecretsListCmd),
4187 #[command(about = "Fetch one secret binding metadata or value")]
4188 Get(WorkersSecretsGetCmd),
4189 #[command(about = "Create or update a secret binding")]
4190 Put(WorkersSecretsPutCmd),
4191 #[command(about = "Delete a secret binding")]
4192 Delete(WorkersSecretsDeleteCmd),
4193 #[command(about = "Create, update, or delete multiple secret bindings from a file")]
4194 Bulk(WorkersSecretsBulkCmd),
4195}
4196
4197#[derive(Args, Debug, Default)]
4198pub struct WorkersSecretsListCmd {}
4199
4200#[derive(Args, Debug)]
4201pub struct WorkersSecretsGetCmd {
4202 #[arg(long, help = "Secret binding name")]
4203 pub name: String,
4204}
4205
4206#[derive(Args, Debug)]
4207pub struct WorkersSecretsPutCmd {
4208 #[arg(long, help = "Secret binding name")]
4209 pub name: String,
4210 #[arg(long, help = "Secret value")]
4211 pub value: Option<String>,
4212 #[arg(long, help = "Read the secret value from stdin instead of --value")]
4213 pub from_stdin: bool,
4214}
4215
4216#[derive(Args, Debug)]
4217pub struct WorkersSecretsDeleteCmd {
4218 #[arg(long, help = "Secret binding name")]
4219 pub name: String,
4220}
4221
4222#[derive(Args, Debug)]
4223pub struct WorkersSecretsBulkCmd {
4224 #[arg(long, help = "Path to a .env or JSON file containing secret updates")]
4225 pub file: PathBuf,
4226 #[arg(
4227 long,
4228 default_value = "env",
4229 help = "Input format: env or json. For json, pass an object mapping names to string values or null for deletes."
4230 )]
4231 pub format: String,
4232}
4233
4234#[derive(Args, Debug)]
4235pub struct WorkersSettingsCmd {
4236 #[command(flatten)]
4237 pub target: WorkersTargetArgs,
4238}
4239
4240#[derive(Args, Debug)]
4241pub struct WorkersDurableObjectsCmd {
4242 #[command(flatten)]
4243 pub target: WorkersTargetArgs,
4244 #[command(subcommand)]
4245 pub command: WorkersDurableObjectsSubCommand,
4246}
4247
4248#[derive(Subcommand, Debug)]
4249pub enum WorkersDurableObjectsSubCommand {
4250 #[command(about = "List configured and live Durable Object namespaces")]
4251 List(WorkersDurableObjectsListCmd),
4252 #[command(about = "Compare local Durable Object configuration with live namespaces")]
4253 Inspect(WorkersDurableObjectsInspectCmd),
4254 #[command(about = "Import live namespace metadata into XBP configuration")]
4255 Sync(WorkersDurableObjectsSyncCmd),
4256}
4257
4258#[derive(Args, Debug, Default)]
4259pub struct WorkersDurableObjectsListCmd {
4260 #[arg(long, help = "Include every namespace in the account")]
4261 pub all: bool,
4262 #[arg(long, help = "Output JSON")]
4263 pub json: bool,
4264}
4265
4266#[derive(Args, Debug, Default)]
4267pub struct WorkersDurableObjectsInspectCmd {
4268 #[arg(long, help = "Include every namespace in the account")]
4269 pub all: bool,
4270 #[arg(long, help = "Output JSON")]
4271 pub json: bool,
4272}
4273
4274#[derive(Args, Debug, Default)]
4275pub struct WorkersDurableObjectsSyncCmd {
4276 #[arg(
4277 long,
4278 help = "Write imported namespace metadata to the local XBP config"
4279 )]
4280 pub apply: bool,
4281 #[arg(
4282 long,
4283 help = "Confirm generation of deploy-affecting migration changes"
4284 )]
4285 pub confirm_migrations: bool,
4286}
4287
4288#[derive(Args, Debug)]
4289pub struct WorkersWranglerCmd {
4290 #[command(subcommand)]
4291 pub command: WorkersWranglerSubCommand,
4292}
4293
4294#[derive(Subcommand, Debug)]
4295pub enum WorkersWranglerSubCommand {
4296 #[command(about = "Generate a Wrangler deploy config JSON file from env vars")]
4297 GenerateConfig(WorkersWranglerGenerateConfigCmd),
4298 #[command(about = "Resolve which Wrangler config file local dev should use")]
4299 ConfigPath(WorkersWranglerConfigPathCmd),
4300 #[command(
4301 name = "run",
4302 alias = "exec",
4303 about = "Run any official Wrangler command from the selected Worker root",
4304 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"
4305 )]
4306 Run(WorkersWranglerRunCmd),
4307}
4308
4309#[derive(Args, Debug)]
4310pub struct WorkersWranglerGenerateConfigCmd {
4311 #[arg(
4312 long,
4313 default_value = "wrangler.deploy.json",
4314 help = "Output filename, relative to the worker root unless absolute"
4315 )]
4316 pub output: PathBuf,
4317}
4318
4319#[derive(Args, Debug)]
4320pub struct WorkersWranglerConfigPathCmd {
4321 #[arg(
4322 long,
4323 default_value = "serve",
4324 help = "Calling command name, for example serve"
4325 )]
4326 pub command_name: String,
4327 #[arg(
4328 long,
4329 default_value = "development",
4330 help = "Execution mode, for example development or production"
4331 )]
4332 pub mode: String,
4333}
4334
4335#[derive(Args, Debug)]
4336pub struct WorkersWranglerRunCmd {
4337 #[arg(
4338 required = true,
4339 trailing_var_arg = true,
4340 allow_hyphen_values = true,
4341 help = "Arguments passed verbatim to the official Wrangler CLI"
4342 )]
4343 pub args: Vec<String>,
4344}
4345
4346#[derive(Args, Debug)]
4347pub struct WorkersD1Cmd {
4348 #[command(subcommand)]
4349 pub command: WorkersD1SubCommand,
4350}
4351
4352#[derive(Subcommand, Debug)]
4353pub enum WorkersD1SubCommand {
4354 #[command(about = "Apply pending Wrangler D1 migrations")]
4355 Migrations(WorkersD1MigrationsCmd),
4356}
4357
4358#[derive(Args, Debug)]
4359pub struct WorkersD1MigrationsCmd {
4360 #[command(subcommand)]
4361 pub command: WorkersD1MigrationsSubCommand,
4362}
4363
4364#[derive(Subcommand, Debug)]
4365pub enum WorkersD1MigrationsSubCommand {
4366 #[command(about = "Apply pending migrations to a local or remote D1 database")]
4367 Apply(WorkersD1MigrationsApplyCmd),
4368}
4369
4370#[derive(Args, Debug)]
4371pub struct WorkersD1MigrationsApplyCmd {
4372 #[arg(help = "D1 database binding or name, for example DB")]
4373 pub database: String,
4374 #[arg(
4375 long,
4376 conflicts_with = "remote",
4377 help = "Apply migrations to the local Wrangler D1 database"
4378 )]
4379 pub local: bool,
4380 #[arg(
4381 long,
4382 conflicts_with = "local",
4383 help = "Apply migrations to the remote D1 database"
4384 )]
4385 pub remote: bool,
4386 #[arg(long, help = "Wrangler config path override")]
4387 pub config: Option<PathBuf>,
4388 #[command(flatten)]
4389 pub target: WorkersTargetArgs,
4390 #[arg(
4391 long,
4392 help = "Persist local D1 state to this directory. When omitted in a git worktree, xbp uses the shared .wrangler/state path automatically."
4393 )]
4394 pub persist_to: Option<PathBuf>,
4395 #[arg(
4396 long,
4397 help = "Disable the automatic shared .wrangler/state path when running local migrations from a git worktree"
4398 )]
4399 pub no_shared_worktree_state: bool,
4400}
4401
4402#[derive(Args, Debug)]
4403pub struct WorkersDeployCmd {
4404 #[arg(
4405 long,
4406 help = "Run the deploy action for every worker app configured in .xbp/xbp.toml"
4407 )]
4408 pub all: bool,
4409 #[command(subcommand)]
4410 pub command: WorkersDeploySubCommand,
4411}
4412
4413#[derive(Subcommand, Debug)]
4414pub enum WorkersDeploySubCommand {
4415 #[command(
4416 about = "Discover worker apps, sync Wrangler config, and optionally write .xbp/xbp.toml"
4417 )]
4418 Configure(WorkersDeployConfigureCmd),
4419 #[command(about = "Run the configured deploy command for one or more worker apps")]
4420 Run(WorkersDeployRunCmd),
4421 #[command(about = "Run the predeploy sync flow unless Workers CI mode is active")]
4422 Predeploy(WorkersDeployPredeployCmd),
4423 #[command(about = "Read .env.local or process env, then emit .dev.vars and Wrangler configs")]
4424 SyncEnvLocal(WorkersDeploySyncEnvLocalCmd),
4425 #[command(
4426 about = "Run the built-in Cloudflare Worker CI deploy workflow (build + wrangler deploy)"
4427 )]
4428 Ci(WorkersDeployCiCmd),
4429 #[command(
4430 about = "Run the existing deploy-selection flow that chooses CI or local deploy behavior"
4431 )]
4432 Select(WorkersDeploySelectCmd),
4433}
4434
4435#[derive(Args, Debug, Default)]
4436pub struct WorkersDeployConfigureCmd {
4437 #[arg(
4438 long,
4439 help = "Scan the repo for Wrangler projects and merge them into .xbp/xbp.toml `workers:`"
4440 )]
4441 pub write_config: bool,
4442 #[arg(
4443 long,
4444 help = "Preview worker discovery and config writes without changing files"
4445 )]
4446 pub dry_run: bool,
4447}
4448
4449#[derive(Args, Debug, Default)]
4450pub struct WorkersDeployRunCmd {}
4451
4452#[derive(Args, Debug)]
4453pub struct WorkersDeployPredeployCmd {
4454 #[arg(long, help = "Force Workers CI mode and skip local sync")]
4455 pub ci: bool,
4456}
4457
4458#[derive(Args, Debug)]
4459pub struct WorkersDeploySyncEnvLocalCmd {}
4460
4461#[derive(Args, Debug)]
4462pub struct WorkersDeployCiCmd {
4463 #[arg(long, help = "Upload a new version without immediately deploying it")]
4464 pub version_upload: bool,
4465}
4466
4467#[derive(Args, Debug)]
4468pub struct WorkersDeploySelectCmd {
4469 #[arg(long, help = "Force the WORKERS_CI=1 branch of the selector")]
4470 pub ci: bool,
4471 #[arg(
4472 long,
4473 help = "Branch name to expose as WORKERS_CI_BRANCH when --ci is set"
4474 )]
4475 pub branch: Option<String>,
4476}
4477
4478#[derive(Args, Debug)]
4479pub struct WorkersWorktreeCmd {
4480 #[command(subcommand)]
4481 pub command: WorkersWorktreeSubCommand,
4482}
4483
4484#[derive(Subcommand, Debug)]
4485pub enum WorkersWorktreeSubCommand {
4486 #[command(about = "Print repo-root, primary worktree, and shared Wrangler state paths")]
4487 Paths(WorkersWorktreePathsCmd),
4488 #[command(
4489 about = "Symlink apps/web/.dev.vars and wrangler.dev.jsonc from the primary worktree when in a linked worktree"
4490 )]
4491 LinkDevVars(WorkersWorktreeLinkDevVarsCmd),
4492}
4493
4494#[derive(Args, Debug, Default)]
4495pub struct WorkersWorktreePathsCmd {
4496 #[arg(
4499 long,
4500 help = "Accepted for compatibility; worktree paths are repo-scoped (no Worker app selection)"
4501 )]
4502 pub all: bool,
4503 #[arg(
4504 long,
4505 help = "Accepted for compatibility; worktree paths are repo-scoped (no Worker app selection)"
4506 )]
4507 pub app: Option<String>,
4508}
4509
4510#[derive(Args, Debug, Default)]
4511pub struct WorkersWorktreeLinkDevVarsCmd {
4512 #[arg(
4513 long,
4514 help = "Accepted for compatibility; worktree helpers are repo-scoped (no Worker app selection)"
4515 )]
4516 pub all: bool,
4517 #[arg(
4518 long,
4519 help = "Accepted for compatibility; worktree helpers are repo-scoped (no Worker app selection)"
4520 )]
4521 pub app: Option<String>,
4522}
4523
4524#[derive(Args, Debug)]
4525pub struct WorkersEnvCmd {
4526 #[command(flatten)]
4527 pub target: WorkersTargetArgs,
4528 #[arg(
4529 long,
4530 help = "Show resolved plain-text binding values instead of masking them"
4531 )]
4532 pub show_values: bool,
4533}
4534
4535#[derive(Args, Debug)]
4536#[command(
4537 arg_required_else_help = true,
4538 disable_help_subcommand = true,
4539 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
4540 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"
4541)]
4542pub struct CloudflareCmd {
4543 #[arg(
4544 long,
4545 global = true,
4546 help = "XBP project root (defaults to the nearest directory containing .xbp/xbp.toml)"
4547 )]
4548 pub root: Option<PathBuf>,
4549 #[arg(
4550 long,
4551 global = true,
4552 help = "Worker app name from .xbp/xbp.toml `workers:`"
4553 )]
4554 pub app: Option<String>,
4555 #[arg(long, global = true, help = "Cloudflare API token override")]
4556 pub token: Option<String>,
4557 #[arg(long, global = true, help = "Cloudflare account ID override")]
4558 pub account_id: Option<String>,
4559 #[command(subcommand)]
4560 pub command: CloudflareSubCommand,
4561}
4562
4563#[derive(Subcommand, Debug)]
4564pub enum CloudflareSubCommand {
4565 #[command(
4566 about = "Validate Wrangler, container metadata, bindings, secrets, and deploy readiness"
4567 )]
4568 Doctor(CloudflareDoctorCmd),
4569 #[command(about = "Discover or write the project-level Worker container contract")]
4570 Init(CloudflareInitCmd),
4571 #[command(about = "Run the canonical Worker + Container deploy workflow")]
4572 Deploy(CloudflareDeployCmd),
4573 #[command(about = "Sync a version-bearing deploy var, deploy, and verify live health")]
4574 Release(CloudflareReleaseCmd),
4575 #[command(about = "Wrap official Wrangler container commands from the selected app root")]
4576 Containers(CloudflareContainersCmd),
4577 #[command(
4578 about = "Manage Cloudflare Tunnels through official Wrangler commands",
4579 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"
4580 )]
4581 Tunnel(CloudflareTunnelCmd),
4582 #[command(
4583 about = "Manage Cloudflare Workflows through official Wrangler commands",
4584 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"
4585 )]
4586 Workflows(CloudflareWorkflowsCmd),
4587 #[command(
4588 about = "Run Wrangler local-development commands with environment-specific vars",
4589 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"
4590 )]
4591 Env(CloudflareEnvCmd),
4592 #[command(about = "Manage local file-backed Cloudflare workflow jobs")]
4593 Jobs(CloudflareJobsCmd),
4594}
4595
4596#[derive(Args, Debug, Default)]
4597pub struct CloudflareDoctorCmd {
4598 #[arg(
4599 long,
4600 help = "Skip live Wrangler calls and only validate local files/config"
4601 )]
4602 pub offline: bool,
4603}
4604
4605#[derive(Args, Debug)]
4606pub struct CloudflareInitCmd {
4607 #[arg(long, help = "Worker app root to store in .xbp/xbp.toml")]
4608 pub worker_root: PathBuf,
4609 #[arg(long, help = "Container port exposed by the runtime")]
4610 pub container_port: u16,
4611 #[arg(
4612 long,
4613 default_value = "/health",
4614 help = "Container health endpoint path"
4615 )]
4616 pub health_path: String,
4617 #[arg(long, help = "Container Durable Object class name")]
4618 pub class_name: Option<String>,
4619 #[arg(long, help = "Worker binding name for the container Durable Object")]
4620 pub binding: Option<String>,
4621 #[arg(
4622 long,
4623 default_value = "Dockerfile",
4624 help = "Dockerfile path relative to the Worker root"
4625 )]
4626 pub dockerfile: PathBuf,
4627 #[arg(long, help = "Cloudflare container application id")]
4628 pub application_id: Option<String>,
4629 #[arg(
4630 long = "required-secret",
4631 help = "Required secret/env binding name",
4632 value_name = "NAME"
4633 )]
4634 pub required_secrets: Vec<String>,
4635 #[arg(long, help = "Wrangler vars key that should carry release versions")]
4636 pub version_var: Option<String>,
4637 #[arg(
4638 long,
4639 help = "Persist the discovered/scaffolded contract to .xbp/xbp.toml"
4640 )]
4641 pub write: bool,
4642}
4643
4644#[derive(Args, Debug)]
4645pub struct CloudflareDeployCmd {
4646 #[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
4647 pub rollout: CloudflareRollout,
4648 #[arg(long, help = "Run validation and Wrangler deploy --dry-run only")]
4649 pub dry_run: bool,
4650 #[arg(
4651 long,
4652 help = "Run post-deploy verification without invoking Wrangler deploy"
4653 )]
4654 pub skip_deploy: bool,
4655 #[arg(
4656 long,
4657 help = "Allow the container image tag to remain unchanged after deploy verification"
4658 )]
4659 pub allow_unchanged_container_image: bool,
4660 #[arg(
4661 long,
4662 help = "Delete old container image tags after successful verification"
4663 )]
4664 pub prune_old_images: bool,
4665 #[arg(long, help = "Image tag count to retain when pruning old tags")]
4666 pub keep_image_tag_count: Option<usize>,
4667}
4668
4669#[derive(Args, Debug)]
4670pub struct CloudflareReleaseCmd {
4671 #[arg(long, help = "Release version to write to the configured version var")]
4672 pub version: String,
4673 #[arg(
4674 long,
4675 help = "Release domain that must validate before this container-backed Worker release"
4676 )]
4677 pub domain: Option<String>,
4678 #[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
4679 pub rollout: CloudflareRollout,
4680 #[arg(
4681 long,
4682 help = "Run release verification without invoking Wrangler deploy"
4683 )]
4684 pub skip_deploy: bool,
4685 #[arg(
4686 long,
4687 help = "Allow the container image tag to remain unchanged after release verification"
4688 )]
4689 pub allow_unchanged_container_image: bool,
4690 #[arg(
4691 long,
4692 help = "Delete old container image tags after successful verification"
4693 )]
4694 pub prune_old_images: bool,
4695 #[arg(long, help = "Image tag count to retain when pruning old tags")]
4696 pub keep_image_tag_count: Option<usize>,
4697}
4698
4699#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
4700#[serde(rename_all = "kebab-case")]
4701pub enum CloudflareRollout {
4702 Immediate,
4703 Gradual,
4704 None,
4705}
4706
4707impl CloudflareRollout {
4708 pub fn as_str(self) -> &'static str {
4709 match self {
4710 Self::Immediate => "immediate",
4711 Self::Gradual => "gradual",
4712 Self::None => "none",
4713 }
4714 }
4715}
4716
4717#[derive(Args, Debug)]
4718pub struct CloudflareContainersCmd {
4719 #[command(subcommand)]
4720 pub command: CloudflareContainersSubCommand,
4721}
4722
4723#[derive(Args, Debug)]
4724pub struct CloudflareTunnelCmd {
4725 #[command(subcommand)]
4726 pub command: CloudflareTunnelSubCommand,
4727}
4728
4729#[derive(Subcommand, Debug)]
4730pub enum CloudflareTunnelSubCommand {
4731 #[command(about = "Create a remotely managed Cloudflare Tunnel")]
4732 Create(CloudflareTunnelCreateCmd),
4733 #[command(about = "Delete a Cloudflare Tunnel")]
4734 Delete(CloudflareTunnelDeleteCmd),
4735 #[command(about = "Show details for a Cloudflare Tunnel")]
4736 Info(CloudflareTunnelInfoCmd),
4737 #[command(about = "List Cloudflare Tunnels")]
4738 List(CloudflareTunnelListCmd),
4739 #[command(about = "Run a named or token-authenticated Cloudflare Tunnel")]
4740 Run(CloudflareTunnelRunCmd),
4741 #[command(about = "Start a temporary anonymous Quick Tunnel")]
4742 QuickStart(CloudflareTunnelQuickStartCmd),
4743}
4744
4745#[derive(Args, Debug)]
4746pub struct CloudflareTunnelCreateCmd {
4747 #[arg(help = "Unique tunnel name within the Cloudflare account")]
4748 pub name: String,
4749}
4750
4751#[derive(Args, Debug)]
4752pub struct CloudflareTunnelDeleteCmd {
4753 #[arg(help = "Tunnel name or UUID")]
4754 pub tunnel: String,
4755 #[arg(long, help = "Skip the confirmation prompt")]
4756 pub force: bool,
4757}
4758
4759#[derive(Args, Debug)]
4760pub struct CloudflareTunnelInfoCmd {
4761 #[arg(help = "Tunnel name or UUID")]
4762 pub tunnel: String,
4763}
4764
4765#[derive(Args, Debug, Default)]
4766pub struct CloudflareTunnelListCmd {}
4767
4768#[derive(Args, Debug)]
4769pub struct CloudflareTunnelRunCmd {
4770 #[arg(help = "Tunnel name or UUID; omit when using --token")]
4771 pub tunnel: Option<String>,
4772 #[arg(long, help = "Tunnel token; avoids API lookup and authentication")]
4773 pub token: Option<String>,
4774 #[arg(
4775 long,
4776 help = "cloudflared log level: debug, info, warn, error, or fatal"
4777 )]
4778 pub log_level: Option<String>,
4779}
4780
4781#[derive(Args, Debug)]
4782pub struct CloudflareTunnelQuickStartCmd {
4783 #[arg(help = "Local URL to expose, for example http://localhost:8080")]
4784 pub url: String,
4785}
4786
4787#[derive(Args, Debug)]
4788pub struct CloudflareWorkflowsCmd {
4789 #[command(subcommand)]
4790 pub command: CloudflareWorkflowsSubCommand,
4791}
4792
4793#[derive(Subcommand, Debug)]
4794pub enum CloudflareWorkflowsSubCommand {
4795 #[command(external_subcommand)]
4796 External(Vec<String>),
4797}
4798
4799#[derive(Args, Debug)]
4800pub struct CloudflareEnvCmd {
4801 #[command(subcommand)]
4802 pub command: CloudflareEnvSubCommand,
4803}
4804
4805#[derive(Subcommand, Debug)]
4806pub enum CloudflareEnvSubCommand {
4807 #[command(external_subcommand)]
4808 External(Vec<String>),
4809}
4810
4811#[derive(Subcommand, Debug)]
4812pub enum CloudflareContainersSubCommand {
4813 #[command(about = "Run wrangler containers list")]
4814 List(CloudflareContainersListCmd),
4815 #[command(
4816 about = "Run wrangler containers info for the configured or supplied application id"
4817 )]
4818 Info(CloudflareContainersInfoCmd),
4819 #[command(
4820 about = "Run wrangler containers instances for the configured or supplied application id"
4821 )]
4822 Instances(CloudflareContainersInstancesCmd),
4823 #[command(about = "Run wrangler containers push")]
4824 Push(CloudflareContainersPushCmd),
4825 #[command(about = "Open wrangler containers ssh for an instance")]
4826 Ssh(CloudflareContainersSshCmd),
4827}
4828
4829#[derive(Args, Debug, Default)]
4830pub struct CloudflareContainersListCmd {
4831 #[arg(long, help = "Ask Wrangler for JSON output when supported")]
4832 pub json: bool,
4833}
4834
4835#[derive(Args, Debug, Default)]
4836pub struct CloudflareContainersInfoCmd {
4837 #[arg(long, help = "Container application id override")]
4838 pub application_id: Option<String>,
4839 #[arg(long, help = "Ask Wrangler for JSON output when supported")]
4840 pub json: bool,
4841}
4842
4843#[derive(Args, Debug, Default)]
4844pub struct CloudflareContainersInstancesCmd {
4845 #[arg(long, help = "Container application id override")]
4846 pub application_id: Option<String>,
4847 #[arg(long, help = "Ask Wrangler for JSON output when supported")]
4848 pub json: bool,
4849}
4850
4851#[derive(Args, Debug)]
4852pub struct CloudflareContainersPushCmd {
4853 #[arg(help = "Image tag or image name to push through Wrangler")]
4854 pub image: String,
4855}
4856
4857#[derive(Args, Debug)]
4858pub struct CloudflareContainersSshCmd {
4859 #[arg(help = "Container instance id")]
4860 pub instance_id: String,
4861 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
4862 pub args: Vec<String>,
4863}
4864
4865#[derive(Args, Debug)]
4866pub struct CloudflareJobsCmd {
4867 #[command(subcommand)]
4868 pub command: CloudflareJobsSubCommand,
4869}
4870
4871#[derive(Subcommand, Debug)]
4872pub enum CloudflareJobsSubCommand {
4873 #[command(about = "Queue a local Cloudflare workflow job")]
4874 Enqueue(CloudflareJobsEnqueueCmd),
4875 #[command(about = "Run queued local Cloudflare workflow jobs")]
4876 Run(CloudflareJobsRunCmd),
4877 #[command(about = "Show one local Cloudflare workflow job")]
4878 Status(CloudflareJobsStatusCmd),
4879 #[command(about = "List local Cloudflare workflow jobs")]
4880 List(CloudflareJobsListCmd),
4881 #[command(about = "Print stored logs for one local Cloudflare workflow job")]
4882 Logs(CloudflareJobsLogsCmd),
4883}
4884
4885#[derive(Args, Debug)]
4886pub struct CloudflareJobsEnqueueCmd {
4887 #[arg(value_enum)]
4888 pub workflow: CloudflareJobWorkflow,
4889 #[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
4890 pub rollout: CloudflareRollout,
4891 #[arg(long, help = "Required for release jobs")]
4892 pub version: Option<String>,
4893 #[arg(long, help = "Queue deploy jobs in dry-run mode")]
4894 pub dry_run: bool,
4895 #[arg(long, help = "Queue verification without invoking Wrangler deploy")]
4896 pub skip_deploy: bool,
4897 #[arg(long, help = "Allow an unchanged container image during verification")]
4898 pub allow_unchanged_container_image: bool,
4899 #[arg(long, help = "Prune old container image tags after verification")]
4900 pub prune_old_images: bool,
4901 #[arg(long, help = "Image tag count to retain when pruning old tags")]
4902 pub keep_image_tag_count: Option<usize>,
4903}
4904
4905#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
4906#[serde(rename_all = "kebab-case")]
4907pub enum CloudflareJobWorkflow {
4908 Doctor,
4909 Deploy,
4910 Release,
4911}
4912
4913impl CloudflareJobWorkflow {
4914 pub fn as_str(self) -> &'static str {
4915 match self {
4916 Self::Doctor => "doctor",
4917 Self::Deploy => "deploy",
4918 Self::Release => "release",
4919 }
4920 }
4921}
4922
4923#[derive(Args, Debug, Default)]
4924pub struct CloudflareJobsRunCmd {
4925 #[arg(long, help = "Run only the next queued job")]
4926 pub once: bool,
4927}
4928
4929#[derive(Args, Debug)]
4930pub struct CloudflareJobsStatusCmd {
4931 pub job_id: String,
4932}
4933
4934#[derive(Args, Debug, Default)]
4935pub struct CloudflareJobsListCmd {}
4936
4937#[derive(Args, Debug)]
4938pub struct CloudflareJobsLogsCmd {
4939 pub job_id: String,
4940}
4941
4942#[cfg(feature = "secrets")]
4943#[derive(Args, Debug)]
4944pub struct SecretsCmd {
4945 #[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
4946 pub provider: SecretsProviderKind,
4947 #[arg(long, help = "GitHub repository override (owner/repo)")]
4948 pub repo: Option<String>,
4949 #[arg(
4950 long,
4951 help = "Provider token override (GitHub token or Cloudflare API token)"
4952 )]
4953 pub token: Option<String>,
4954 #[arg(long, help = "Cloudflare account ID override")]
4955 pub account_id: Option<String>,
4956 #[arg(
4957 long = "environment",
4958 alias = "env",
4959 default_value = "xbp-dev",
4960 help = "Environment to sync (default: xbp-dev). Nested services are scoped automatically, e.g. xbp-dev-web."
4961 )]
4962 pub environment: String,
4963 #[arg(
4964 long,
4965 help = "Service name from .xbp/xbp.toml. If omitted, XBP resolves it from the current directory or prompts when ambiguous."
4966 )]
4967 pub service: Option<String>,
4968 #[command(subcommand)]
4969 pub command: Option<SecretsSubCommand>,
4970}
4971
4972#[cfg(feature = "secrets")]
4973#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
4974pub enum SecretsProviderKind {
4975 Github,
4976 Cloudflare,
4977 Railway,
4978 Vercel,
4979}
4980
4981#[cfg(feature = "secrets")]
4982#[derive(Subcommand, Debug)]
4983pub enum SecretsSubCommand {
4984 #[command(alias = "ls", alias = "list-providers")]
4986 Providers,
4987 List(ListCmd),
4989 Push(PushCmd),
4991 Pull(PullCmd),
4993 GenerateDefault(GenerateDefaultCmd),
4995 GenerateExample(GenerateExampleCmd),
4997 Diff,
4999 Verify,
5001 #[command(name = "diag", alias = "doctor")]
5003 Diag,
5004 Stores(SecretsStoresCmd),
5006 Secrets(CloudflareSecretsCmd),
5008 Quota(SecretsQuotaCmd),
5010 #[command(name = "usage")]
5012 Usage,
5013}
5014
5015#[cfg(feature = "secrets")]
5016#[derive(Args, Debug)]
5017pub struct ListCmd {
5018 #[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
5019 pub file: Option<String>,
5020 #[arg(long, help = "Output format: plain (default) or json")]
5021 pub format: Option<String>,
5022}
5023
5024#[cfg(feature = "secrets")]
5025#[derive(Args, Debug)]
5026pub struct PushCmd {
5027 #[arg(long, help = "Path to env file (default: .env.local/.env)")]
5028 pub file: Option<String>,
5029 #[arg(
5030 long = "dev-vars-file",
5031 help = "Path to Cloudflare .dev.vars file. Defaults to .dev.vars when it exists."
5032 )]
5033 pub dev_vars_file: Option<String>,
5034 #[arg(
5035 long,
5036 help = "Skip the Cloudflare .dev.vars GitHub environment sync lane"
5037 )]
5038 pub skip_dev_vars: bool,
5039 #[arg(
5040 long,
5041 help = "Force overwrite existing GitHub Actions environment variables"
5042 )]
5043 pub force: bool,
5044 #[arg(long, help = "Show what would be pushed without making changes")]
5045 pub dry_run: bool,
5046}
5047
5048#[cfg(feature = "secrets")]
5049#[derive(Args, Debug)]
5050pub struct PullCmd {
5051 #[arg(long, help = "Output file path (default: .env.local)")]
5052 pub output: Option<String>,
5053 #[arg(
5054 long = "dev-vars-output",
5055 help = "Output path for Cloudflare .dev.vars. Defaults to .dev.vars when it already exists."
5056 )]
5057 pub dev_vars_output: Option<String>,
5058 #[arg(
5059 long,
5060 help = "Pull the Cloudflare .dev.vars environment even when .dev.vars does not exist yet"
5061 )]
5062 pub include_dev_vars: bool,
5063 #[arg(
5064 long,
5065 help = "Skip the Cloudflare .dev.vars GitHub environment sync lane"
5066 )]
5067 pub skip_dev_vars: bool,
5068}
5069
5070#[cfg(feature = "secrets")]
5071#[derive(Args, Debug)]
5072pub struct GenerateDefaultCmd {
5073 #[arg(long, help = "Output file path (default: .env.default)")]
5074 pub output: Option<String>,
5075}
5076
5077#[cfg(feature = "secrets")]
5078#[derive(Args, Debug)]
5079pub struct GenerateExampleCmd {
5080 #[arg(long, help = "Output file path (default: .env.example)")]
5081 pub output: Option<String>,
5082 #[arg(long, help = "Remove keys from .env.local not in .env.example")]
5083 pub clean: bool,
5084 #[arg(long, help = "Only include vars matching prefix (repeatable)")]
5085 pub include_prefix: Vec<String>,
5086 #[arg(long, help = "Exclude vars matching prefix (repeatable)")]
5087 pub exclude_prefix: Vec<String>,
5088}
5089
5090#[cfg(feature = "secrets")]
5091#[derive(Args, Debug)]
5092pub struct SecretsStoresCmd {
5093 #[command(subcommand)]
5094 pub command: SecretsStoresSubCommand,
5095}
5096
5097#[cfg(feature = "secrets")]
5098#[derive(Subcommand, Debug)]
5099pub enum SecretsStoresSubCommand {
5100 List(CloudflareSecretsStoreListCmd),
5101 Get(CloudflareSecretsStoreGetCmd),
5102 Create(CloudflareSecretsStoreCreateCmd),
5103 Delete(CloudflareSecretsStoreDeleteCmd),
5104}
5105
5106#[cfg(feature = "secrets")]
5107#[derive(Args, Debug)]
5108pub struct CloudflareSecretsStoreListCmd {}
5109
5110#[cfg(feature = "secrets")]
5111#[derive(Args, Debug)]
5112pub struct CloudflareSecretsStoreGetCmd {
5113 #[arg(long)]
5114 pub store_id: String,
5115}
5116
5117#[cfg(feature = "secrets")]
5118#[derive(Args, Debug)]
5119pub struct CloudflareSecretsStoreCreateCmd {
5120 #[arg(long)]
5121 pub name: String,
5122}
5123
5124#[cfg(feature = "secrets")]
5125#[derive(Args, Debug)]
5126pub struct CloudflareSecretsStoreDeleteCmd {
5127 #[arg(long)]
5128 pub store_id: String,
5129}
5130
5131#[cfg(feature = "secrets")]
5132#[derive(Args, Debug)]
5133pub struct CloudflareSecretsCmd {
5134 #[command(subcommand)]
5135 pub command: CloudflareSecretsSubCommand,
5136}
5137
5138#[cfg(feature = "secrets")]
5139#[derive(Subcommand, Debug)]
5140pub enum CloudflareSecretsSubCommand {
5141 List(CloudflareSecretsListCmd),
5142 Get(CloudflareSecretsGetCmd),
5143 Create(CloudflareSecretsCreateCmd),
5144 Edit(CloudflareSecretsEditCmd),
5145 Delete(CloudflareSecretsDeleteCmd),
5146 #[command(name = "delete-bulk")]
5147 DeleteBulk(CloudflareSecretsBulkDeleteCmd),
5148 Duplicate(CloudflareSecretsDuplicateCmd),
5149}
5150
5151#[cfg(feature = "secrets")]
5152#[derive(Args, Debug)]
5153pub struct CloudflareSecretsListCmd {
5154 #[arg(long)]
5155 pub store_id: String,
5156}
5157
5158#[cfg(feature = "secrets")]
5159#[derive(Args, Debug)]
5160pub struct CloudflareSecretsGetCmd {
5161 #[arg(long)]
5162 pub store_id: String,
5163 #[arg(long)]
5164 pub secret_id: String,
5165}
5166
5167#[cfg(feature = "secrets")]
5168#[derive(Args, Debug)]
5169pub struct CloudflareSecretsCreateCmd {
5170 #[arg(long)]
5171 pub store_id: String,
5172 #[arg(long)]
5173 pub name: String,
5174 #[arg(long)]
5175 pub value: String,
5176 #[arg(long, value_delimiter = ',')]
5177 pub scopes: Vec<String>,
5178 #[arg(long)]
5179 pub comment: Option<String>,
5180}
5181
5182#[cfg(feature = "secrets")]
5183#[derive(Args, Debug)]
5184pub struct CloudflareSecretsEditCmd {
5185 #[arg(long)]
5186 pub store_id: String,
5187 #[arg(long)]
5188 pub secret_id: String,
5189 #[arg(long)]
5190 pub name: Option<String>,
5191 #[arg(long)]
5192 pub value: Option<String>,
5193 #[arg(long, value_delimiter = ',')]
5194 pub scopes: Vec<String>,
5195 #[arg(long)]
5196 pub comment: Option<String>,
5197}
5198
5199#[cfg(feature = "secrets")]
5200#[derive(Args, Debug)]
5201pub struct CloudflareSecretsDeleteCmd {
5202 #[arg(long)]
5203 pub store_id: String,
5204 #[arg(long)]
5205 pub secret_id: String,
5206}
5207
5208#[cfg(feature = "secrets")]
5209#[derive(Args, Debug)]
5210pub struct CloudflareSecretsBulkDeleteCmd {
5211 #[arg(long)]
5212 pub store_id: String,
5213 #[arg(long = "secret-id", required = true)]
5214 pub secret_ids: Vec<String>,
5215}
5216
5217#[cfg(feature = "secrets")]
5218#[derive(Args, Debug)]
5219pub struct CloudflareSecretsDuplicateCmd {
5220 #[arg(long)]
5221 pub store_id: String,
5222 #[arg(long)]
5223 pub secret_id: String,
5224 #[arg(long)]
5225 pub name: String,
5226 #[arg(long, value_delimiter = ',')]
5227 pub scopes: Vec<String>,
5228 #[arg(long)]
5229 pub comment: Option<String>,
5230}
5231
5232#[cfg(feature = "secrets")]
5233#[derive(Args, Debug)]
5234pub struct SecretsQuotaCmd {
5235 #[command(subcommand)]
5236 pub command: SecretsQuotaSubCommand,
5237}
5238
5239#[cfg(feature = "secrets")]
5240#[derive(Subcommand, Debug)]
5241pub enum SecretsQuotaSubCommand {
5242 Get(SecretsQuotaGetCmd),
5243}
5244
5245#[cfg(feature = "secrets")]
5246#[derive(Args, Debug)]
5247pub struct SecretsQuotaGetCmd {}
5248
5249const DNS_HELP_TEMPLATE: &str = crate::cli::help_render::XBP_HELP_TEMPLATE;
5250
5251const DNS_COMMAND_AFTER_HELP: &str = "\
5252Examples:
5253 xbp dns providers
5254 xbp dns zones list --provider cloudflare --account-id acc_123
5255 xbp dns records list --provider cloudflare --zone-id zone_123
5256 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
5257 xbp dns dnssec get --provider cloudflare --zone-id zone_123
5258 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
5259
5260Notes:
5261 Start with `xbp dns providers` to see what is implemented today.
5262 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`).";
5263
5264const DNS_ZONES_AFTER_HELP: &str = "\
5265Examples:
5266 xbp dns zones list --provider cloudflare --account-id acc_123
5267 xbp dns zones get --provider cloudflare --zone-id zone_123
5268 xbp dns zones create --provider cloudflare --name example.com --account-id acc_123 --jump-start
5269 xbp dns zones edit --provider cloudflare --zone-id zone_123 --paused true
5270 xbp dns zones delete --provider cloudflare --zone-id zone_123";
5271
5272const DNS_RECORDS_AFTER_HELP: &str = "\
5273Examples:
5274 xbp dns records list --provider cloudflare --zone-id zone_123
5275 xbp dns records get --provider cloudflare --zone-id zone_123 --record-id rec_123
5276 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
5277 xbp dns records edit --provider cloudflare --zone-id zone_123 --record-id rec_123 --proxied true
5278 xbp dns records import --provider cloudflare --zone-id zone_123 --file zone.txt
5279 xbp dns records export --provider cloudflare --zone-id zone_123 --output zone.txt";
5280
5281const DNS_DNSSEC_AFTER_HELP: &str = "\
5282Examples:
5283 xbp dns dnssec get --provider cloudflare --zone-id zone_123
5284 xbp dns dnssec edit --provider cloudflare --zone-id zone_123 --status active";
5285
5286const DNS_SETTINGS_AFTER_HELP: &str = "\
5287Examples:
5288 xbp dns settings get --provider cloudflare --zone-id zone_123
5289 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
5290 xbp dns settings edit --provider cloudflare --zone-id zone_123 --nameservers-type custom --nameservers-ns-set 2";
5291
5292const DNS_PROVIDERS_AFTER_HELP: &str = "\
5293Examples:
5294 xbp dns providers
5295
5296What this shows:
5297 Implemented providers are wired into `xbp dns` today.
5298 Planned providers are tracked in the CLI surface but not callable yet.";
5299
5300#[derive(Args, Debug)]
5301#[command(
5302 about = "Manage DNS providers, zones, records, DNSSEC, and provider-level settings",
5303 arg_required_else_help = true,
5304 disable_help_subcommand = true,
5305 help_template = DNS_HELP_TEMPLATE,
5306 after_help = DNS_COMMAND_AFTER_HELP
5307)]
5308pub struct DnsCmd {
5309 #[command(subcommand)]
5310 pub command: DnsSubCommand,
5311}
5312
5313#[derive(Subcommand, Debug)]
5314pub enum DnsSubCommand {
5315 #[command(
5316 alias = "ls",
5317 alias = "list",
5318 about = "List supported DNS providers and current implementation status",
5319 after_help = DNS_PROVIDERS_AFTER_HELP
5320 )]
5321 Providers,
5322 #[command(about = "Inspect and manage DNS zones")]
5323 Zones(DnsZonesCmd),
5324 #[command(about = "List, create, edit, import, export, and batch DNS records")]
5325 Records(DnsRecordsCmd),
5326 #[command(about = "Inspect or edit DNSSEC status for a zone")]
5327 Dnssec(DnssecCmd),
5328 #[command(about = "Inspect or edit provider DNS settings for a zone")]
5329 Settings(DnsSettingsCmd),
5330}
5331
5332#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
5333pub enum DnsProviderKind {
5334 Cloudflare,
5335 Hetzner,
5336 Vercel,
5337 Custom,
5338}
5339
5340#[derive(Args, Debug)]
5341#[command(
5342 about = "Inspect and manage DNS zones",
5343 arg_required_else_help = true,
5344 help_template = DNS_HELP_TEMPLATE,
5345 after_help = DNS_ZONES_AFTER_HELP
5346)]
5347pub struct DnsZonesCmd {
5348 #[command(subcommand)]
5349 pub command: DnsZonesSubCommand,
5350}
5351
5352#[derive(Subcommand, Debug)]
5353pub enum DnsZonesSubCommand {
5354 #[command(about = "List zones for a provider account")]
5355 List(DnsZoneListCmd),
5356 #[command(about = "Fetch one zone by id")]
5357 Get(DnsZoneGetCmd),
5358 #[command(about = "Create a new zone")]
5359 Create(DnsZoneCreateCmd),
5360 #[command(about = "Edit zone-level properties")]
5361 Edit(DnsZoneEditCmd),
5362 #[command(about = "Delete a zone")]
5363 Delete(DnsZoneDeleteCmd),
5364}
5365
5366#[derive(Args, Debug)]
5367pub struct DnsZoneListCmd {
5368 #[arg(long, value_enum, default_value = "cloudflare")]
5369 pub provider: DnsProviderKind,
5370 #[arg(long)]
5371 pub account_id: Option<String>,
5372 #[arg(long)]
5373 pub account_name: Option<String>,
5374 #[arg(long = "account-name-op")]
5375 pub account_name_op: Option<String>,
5376 #[arg(long)]
5377 pub name: Option<String>,
5378 #[arg(long = "name-op")]
5379 pub name_op: Option<String>,
5380 #[arg(long)]
5381 pub status: Option<String>,
5382 #[arg(long = "type", value_delimiter = ',')]
5383 pub zone_types: Vec<String>,
5384 #[arg(long)]
5385 pub r#match: Option<String>,
5386 #[arg(long)]
5387 pub order: Option<String>,
5388 #[arg(long)]
5389 pub direction: Option<String>,
5390 #[arg(long)]
5391 pub page: Option<u64>,
5392 #[arg(long = "per-page")]
5393 pub per_page: Option<u64>,
5394 #[arg(long)]
5395 pub token: Option<String>,
5396}
5397
5398#[derive(Args, Debug)]
5399pub struct DnsZoneGetCmd {
5400 #[arg(long, value_enum, default_value = "cloudflare")]
5401 pub provider: DnsProviderKind,
5402 #[arg(long)]
5403 pub zone_id: String,
5404 #[arg(long)]
5405 pub token: Option<String>,
5406}
5407
5408#[derive(Args, Debug)]
5409pub struct DnsZoneCreateCmd {
5410 #[arg(long, value_enum, default_value = "cloudflare")]
5411 pub provider: DnsProviderKind,
5412 #[arg(long)]
5413 pub name: String,
5414 #[arg(long)]
5415 pub account_id: Option<String>,
5416 #[arg(long)]
5417 pub jump_start: bool,
5418 #[arg(long = "type")]
5419 pub zone_type: Option<String>,
5420 #[arg(long)]
5421 pub token: Option<String>,
5422}
5423
5424#[derive(Args, Debug)]
5425pub struct DnsZoneEditCmd {
5426 #[arg(long, value_enum, default_value = "cloudflare")]
5427 pub provider: DnsProviderKind,
5428 #[arg(long)]
5429 pub zone_id: String,
5430 #[arg(long)]
5431 pub paused: Option<bool>,
5432 #[arg(long = "type")]
5433 pub zone_type: Option<String>,
5434 #[arg(long = "vanity-name-server")]
5435 pub vanity_name_servers: Vec<String>,
5436 #[arg(long)]
5437 pub token: Option<String>,
5438}
5439
5440#[derive(Args, Debug)]
5441pub struct DnsZoneDeleteCmd {
5442 #[arg(long, value_enum, default_value = "cloudflare")]
5443 pub provider: DnsProviderKind,
5444 #[arg(long)]
5445 pub zone_id: String,
5446 #[arg(long)]
5447 pub token: Option<String>,
5448}
5449
5450#[derive(Args, Debug)]
5451#[command(
5452 about = "List, create, edit, import, export, and batch DNS records",
5453 arg_required_else_help = true,
5454 help_template = DNS_HELP_TEMPLATE,
5455 after_help = DNS_RECORDS_AFTER_HELP
5456)]
5457pub struct DnsRecordsCmd {
5458 #[command(subcommand)]
5459 pub command: DnsRecordsSubCommand,
5460}
5461
5462#[derive(Subcommand, Debug)]
5463pub enum DnsRecordsSubCommand {
5464 #[command(about = "List records in a zone")]
5465 List(DnsRecordListCmd),
5466 #[command(about = "Fetch one record by id")]
5467 Get(DnsRecordGetCmd),
5468 #[command(about = "Create a new DNS record")]
5469 Create(DnsRecordCreateCmd),
5470 #[command(about = "Replace a record by id with a full payload")]
5471 Replace(DnsRecordReplaceCmd),
5472 #[command(about = "Patch selected fields on a record")]
5473 Edit(DnsRecordEditCmd),
5474 #[command(about = "Delete a record")]
5475 Delete(DnsRecordDeleteCmd),
5476 #[command(about = "Apply a Cloudflare batch record payload from JSON")]
5477 Batch(DnsRecordBatchCmd),
5478 #[command(about = "Import a BIND-style zone file into a zone")]
5479 Import(DnsRecordImportCmd),
5480 #[command(about = "Export a zone as a BIND-style zone file")]
5481 Export(DnsRecordExportCmd),
5482}
5483
5484#[derive(Args, Debug)]
5485pub struct DnsRecordListCmd {
5486 #[arg(long, value_enum, default_value = "cloudflare")]
5487 pub provider: DnsProviderKind,
5488 #[arg(long)]
5489 pub zone_id: String,
5490 #[arg(long = "type")]
5491 pub record_type: Option<String>,
5492 #[arg(long)]
5493 pub name: Option<String>,
5494 #[arg(long)]
5495 pub page: Option<u64>,
5496 #[arg(long = "per-page")]
5497 pub per_page: Option<u64>,
5498 #[arg(long)]
5499 pub token: Option<String>,
5500}
5501
5502#[derive(Args, Debug)]
5503pub struct DnsRecordGetCmd {
5504 #[arg(long, value_enum, default_value = "cloudflare")]
5505 pub provider: DnsProviderKind,
5506 #[arg(long)]
5507 pub zone_id: String,
5508 #[arg(long)]
5509 pub record_id: String,
5510 #[arg(long)]
5511 pub token: Option<String>,
5512}
5513
5514#[derive(Args, Debug)]
5515pub struct DnsRecordCreateCmd {
5516 #[arg(long, value_enum, default_value = "cloudflare")]
5517 pub provider: DnsProviderKind,
5518 #[arg(long)]
5519 pub zone_id: String,
5520 #[arg(long = "type")]
5521 pub record_type: String,
5522 #[arg(long)]
5523 pub name: String,
5524 #[arg(long)]
5525 pub content: String,
5526 #[arg(long)]
5527 pub ttl: Option<u32>,
5528 #[arg(long)]
5529 pub proxied: Option<bool>,
5530 #[arg(long)]
5531 pub priority: Option<u32>,
5532 #[arg(long)]
5533 pub comment: Option<String>,
5534 #[arg(long = "tag")]
5535 pub tags: Vec<String>,
5536 #[arg(long = "data-json")]
5537 pub data_json: Option<String>,
5538 #[arg(long = "settings-json")]
5539 pub settings_json: Option<String>,
5540 #[arg(long)]
5541 pub token: Option<String>,
5542}
5543
5544#[derive(Args, Debug)]
5545pub struct DnsRecordReplaceCmd {
5546 #[command(flatten)]
5547 pub common: DnsRecordCreateCmd,
5548 #[arg(long)]
5549 pub record_id: String,
5550}
5551
5552#[derive(Args, Debug)]
5553pub struct DnsRecordEditCmd {
5554 #[arg(long, value_enum, default_value = "cloudflare")]
5555 pub provider: DnsProviderKind,
5556 #[arg(long)]
5557 pub zone_id: String,
5558 #[arg(long)]
5559 pub record_id: String,
5560 #[arg(long = "type")]
5561 pub record_type: Option<String>,
5562 #[arg(long)]
5563 pub name: Option<String>,
5564 #[arg(long)]
5565 pub content: Option<String>,
5566 #[arg(long)]
5567 pub ttl: Option<u32>,
5568 #[arg(long)]
5569 pub proxied: Option<bool>,
5570 #[arg(long)]
5571 pub priority: Option<u32>,
5572 #[arg(long)]
5573 pub comment: Option<String>,
5574 #[arg(long = "tag")]
5575 pub tags: Vec<String>,
5576 #[arg(long = "data-json")]
5577 pub data_json: Option<String>,
5578 #[arg(long = "settings-json")]
5579 pub settings_json: Option<String>,
5580 #[arg(long)]
5581 pub token: Option<String>,
5582}
5583
5584#[derive(Args, Debug)]
5585pub struct DnsRecordDeleteCmd {
5586 #[arg(long, value_enum, default_value = "cloudflare")]
5587 pub provider: DnsProviderKind,
5588 #[arg(long)]
5589 pub zone_id: String,
5590 #[arg(long)]
5591 pub record_id: String,
5592 #[arg(long)]
5593 pub token: Option<String>,
5594}
5595
5596#[derive(Args, Debug)]
5597pub struct DnsRecordBatchCmd {
5598 #[arg(long, value_enum, default_value = "cloudflare")]
5599 pub provider: DnsProviderKind,
5600 #[arg(long)]
5601 pub zone_id: String,
5602 #[arg(long)]
5603 pub input: PathBuf,
5604 #[arg(long)]
5605 pub token: Option<String>,
5606}
5607
5608#[derive(Args, Debug)]
5609pub struct DnsRecordImportCmd {
5610 #[arg(long, value_enum, default_value = "cloudflare")]
5611 pub provider: DnsProviderKind,
5612 #[arg(long)]
5613 pub zone_id: String,
5614 #[arg(long)]
5615 pub file: PathBuf,
5616 #[arg(long)]
5617 pub token: Option<String>,
5618}
5619
5620#[derive(Args, Debug)]
5621pub struct DnsRecordExportCmd {
5622 #[arg(long, value_enum, default_value = "cloudflare")]
5623 pub provider: DnsProviderKind,
5624 #[arg(long)]
5625 pub zone_id: String,
5626 #[arg(long)]
5627 pub output: Option<PathBuf>,
5628 #[arg(long)]
5629 pub token: Option<String>,
5630}
5631
5632#[derive(Args, Debug)]
5633#[command(
5634 about = "Inspect or edit DNSSEC status for a zone",
5635 arg_required_else_help = true,
5636 help_template = DNS_HELP_TEMPLATE,
5637 after_help = DNS_DNSSEC_AFTER_HELP
5638)]
5639pub struct DnssecCmd {
5640 #[command(subcommand)]
5641 pub command: DnssecSubCommand,
5642}
5643
5644#[derive(Subcommand, Debug)]
5645pub enum DnssecSubCommand {
5646 #[command(about = "Fetch DNSSEC state for a zone")]
5647 Get(DnssecGetCmd),
5648 #[command(about = "Edit DNSSEC-related flags for a zone")]
5649 Edit(DnssecEditCmd),
5650}
5651
5652#[derive(Args, Debug)]
5653pub struct DnssecGetCmd {
5654 #[arg(long, value_enum, default_value = "cloudflare")]
5655 pub provider: DnsProviderKind,
5656 #[arg(long)]
5657 pub zone_id: String,
5658 #[arg(long)]
5659 pub token: Option<String>,
5660}
5661
5662#[derive(Args, Debug)]
5663pub struct DnssecEditCmd {
5664 #[arg(long, value_enum, default_value = "cloudflare")]
5665 pub provider: DnsProviderKind,
5666 #[arg(long)]
5667 pub zone_id: String,
5668 #[arg(long)]
5669 pub status: Option<String>,
5670 #[arg(long = "dnssec-multi-signer")]
5671 pub dnssec_multi_signer: Option<bool>,
5672 #[arg(long = "dnssec-presigned")]
5673 pub dnssec_presigned: Option<bool>,
5674 #[arg(long = "dnssec-use-nsec3")]
5675 pub dnssec_use_nsec3: Option<bool>,
5676 #[arg(long)]
5677 pub token: Option<String>,
5678}
5679
5680#[derive(Args, Debug)]
5681#[command(
5682 about = "Inspect or edit provider DNS settings for a zone",
5683 arg_required_else_help = true,
5684 help_template = DNS_HELP_TEMPLATE,
5685 after_help = DNS_SETTINGS_AFTER_HELP
5686)]
5687pub struct DnsSettingsCmd {
5688 #[command(subcommand)]
5689 pub command: DnsSettingsSubCommand,
5690}
5691
5692#[derive(Subcommand, Debug)]
5693pub enum DnsSettingsSubCommand {
5694 #[command(about = "Fetch provider DNS settings for a zone")]
5695 Get(DnsSettingsGetCmd),
5696 #[command(about = "Edit provider DNS settings for a zone")]
5697 Edit(DnsSettingsEditCmd),
5698}
5699
5700#[derive(Args, Debug)]
5701pub struct DnsSettingsGetCmd {
5702 #[arg(long, value_enum, default_value = "cloudflare")]
5703 pub provider: DnsProviderKind,
5704 #[arg(long)]
5705 pub zone_id: String,
5706 #[arg(long)]
5707 pub token: Option<String>,
5708}
5709
5710#[derive(Args, Debug)]
5711pub struct DnsSettingsEditCmd {
5712 #[arg(long, value_enum, default_value = "cloudflare")]
5713 pub provider: DnsProviderKind,
5714 #[arg(long)]
5715 pub zone_id: String,
5716 #[arg(long)]
5717 pub flatten_all_cnames: Option<bool>,
5718 #[arg(long)]
5719 pub foundation_dns: Option<bool>,
5720 #[arg(long)]
5721 pub multi_provider: Option<bool>,
5722 #[arg(long)]
5723 pub ns_ttl: Option<u32>,
5724 #[arg(long)]
5725 pub secondary_overrides: Option<bool>,
5726 #[arg(long)]
5727 pub zone_mode: Option<String>,
5728 #[arg(long = "reference-zone-id")]
5729 pub reference_zone_id: Option<String>,
5730 #[arg(long = "nameservers-type")]
5731 pub nameservers_type: Option<String>,
5732 #[arg(long = "nameservers-ns-set")]
5733 pub nameservers_ns_set: Option<u32>,
5734 #[arg(long = "soa-json")]
5735 pub soa_json: Option<String>,
5736 #[arg(long)]
5737 pub token: Option<String>,
5738}
5739
5740#[derive(Args, Debug)]
5741#[command(
5742 arg_required_else_help = true,
5743 disable_help_subcommand = true,
5744 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
5745 after_help = crate::cli::help_render::DOMAINS_AFTER_HELP
5746)]
5747pub struct DomainsCmd {
5748 #[arg(long, value_enum, default_value = "cloudflare")]
5749 pub provider: DomainsProviderKind,
5750 #[arg(long)]
5751 pub account_id: Option<String>,
5752 #[arg(long)]
5753 pub token: Option<String>,
5754 #[command(subcommand)]
5755 pub command: DomainsSubCommand,
5756}
5757
5758#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
5759pub enum DomainsProviderKind {
5760 Cloudflare,
5761}
5762
5763#[derive(Subcommand, Debug)]
5764pub enum DomainsSubCommand {
5765 Search(DomainsSearchCmd),
5766 Check(DomainsCheckCmd),
5767 List(DomainsListCmd),
5768}
5769
5770#[derive(Args, Debug)]
5771pub struct DomainsSearchCmd {
5772 #[arg(long)]
5773 pub query: String,
5774 #[arg(long = "extension")]
5775 pub extensions: Vec<String>,
5776 #[arg(long)]
5777 pub limit: Option<usize>,
5778}
5779
5780#[derive(Args, Debug)]
5781pub struct DomainsCheckCmd {
5782 #[arg(long = "domain", required = true)]
5783 pub domains: Vec<String>,
5784}
5785
5786#[derive(Args, Debug)]
5787pub struct DomainsListCmd {}
5788
5789#[derive(Args, Debug)]
5790pub struct GenerateSystemdCmd {
5791 #[arg(
5792 long,
5793 default_value = "/etc/systemd/system",
5794 help = "Directory where the systemd units are written"
5795 )]
5796 pub output_dir: PathBuf,
5797 #[arg(long, help = "Only generate the unit for this service name")]
5798 pub service: Option<String>,
5799 #[arg(
5800 long,
5801 default_value_t = true,
5802 help = "Also generate the xbp-api systemd unit alongside project/services"
5803 )]
5804 pub api: bool,
5805}
5806
5807#[derive(Args, Debug)]
5808#[command(
5809 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
5810 after_help = crate::cli::help_render::DONE_AFTER_HELP
5811)]
5812pub struct DoneCmd {
5813 #[arg(long, help = "Root directory under which to discover git repos")]
5814 pub root: Option<std::path::PathBuf>,
5815 #[arg(
5816 long,
5817 default_value = "24 hours ago",
5818 help = "Git --since value (e.g. '7 days ago')"
5819 )]
5820 pub since: String,
5821 #[arg(short, long, help = "Output Markdown file path")]
5822 pub output: Option<std::path::PathBuf>,
5823 #[arg(long, help = "Skip AI summarization (OpenRouter)")]
5824 pub no_ai: bool,
5825 #[arg(short, long, help = "Discover repos recursively")]
5826 pub recursive: bool,
5827 #[arg(long, help = "Exclude repo by name (repeatable)")]
5828 pub exclude: Vec<String>,
5829}
5830
5831#[derive(Args, Debug)]
5832pub struct FixProcessMonitorJsonCmd {
5833 #[arg(help = "Path to a Cursor process-monitor JSON export")]
5834 pub path: std::path::PathBuf,
5835 #[arg(
5836 long,
5837 help = "Check whether the file needs repair without writing changes"
5838 )]
5839 pub check: bool,
5840 #[arg(
5841 long,
5842 help = "Print repaired JSON to stdout instead of overwriting the file"
5843 )]
5844 pub stdout: bool,
5845}
5846
5847#[derive(Args, Debug)]
5848pub struct CursorCmd {
5849 #[command(subcommand)]
5850 pub command: CursorSubCommand,
5851}
5852
5853#[derive(Subcommand, Debug)]
5854pub enum CursorSubCommand {
5855 #[command(about = "Upload local Cursor file history to the XBP dashboard")]
5856 Ingest {
5857 #[arg(
5858 long,
5859 help = "Scan local Cursor history without uploading to the dashboard"
5860 )]
5861 dry_run: bool,
5862 },
5863}
5864
5865#[cfg(feature = "nordvpn")]
5866#[derive(Args, Debug)]
5867pub struct NordvpnCmd {
5868 #[arg(
5869 trailing_var_arg = true,
5870 allow_hyphen_values = true,
5871 help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
5872 )]
5873 pub args: Vec<String>,
5874}
5875
5876#[cfg(feature = "kubernetes")]
5877#[derive(Args, Debug)]
5878pub struct KubernetesCmd {
5879 #[command(subcommand)]
5880 pub command: KubernetesSubCommand,
5881}
5882
5883#[cfg(feature = "kubernetes")]
5884#[derive(Args, Debug)]
5885pub struct KubernetesAddonCmd {
5886 #[command(subcommand)]
5887 pub command: KubernetesAddonSubCommand,
5888}
5889
5890#[cfg(feature = "kubernetes")]
5891#[derive(Subcommand, Debug)]
5892pub enum KubernetesAddonSubCommand {
5893 List,
5895 Enable {
5897 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
5898 name: String,
5899 },
5900 Disable {
5902 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
5903 name: String,
5904 },
5905}
5906
5907#[cfg(feature = "kubernetes")]
5908#[derive(Subcommand, Debug)]
5909pub enum KubernetesCrdsSubCommand {
5910 Apply {
5912 #[arg(long, help = "Path to CRDs directory or file")]
5913 path: Option<PathBuf>,
5914 #[arg(long, help = "Service name (reads deploy.envs.*.kubernetes.crds_path)")]
5915 service: Option<String>,
5916 #[arg(long, help = "Deploy environment")]
5917 env: Option<String>,
5918 #[arg(long, help = "Override kube context")]
5919 context: Option<String>,
5920 #[arg(long, help = "Server dry-run")]
5921 dry_run: bool,
5922 #[arg(long, help = "Skip confirmation")]
5923 yes: bool,
5924 },
5925}
5926
5927#[cfg(feature = "kubernetes")]
5928#[derive(Subcommand, Debug)]
5929pub enum KubernetesOperatorSubCommand {
5930 Install {
5932 #[arg(long, help = "Path to operator install manifests")]
5933 path: Option<PathBuf>,
5934 #[arg(long, help = "Service name (reads deploy install_path)")]
5935 service: Option<String>,
5936 #[arg(long, help = "Namespace")]
5937 namespace: Option<String>,
5938 #[arg(long, help = "Deploy environment")]
5939 env: Option<String>,
5940 #[arg(long, help = "Override kube context")]
5941 context: Option<String>,
5942 #[arg(long, help = "Server dry-run")]
5943 dry_run: bool,
5944 #[arg(long, help = "Skip confirmation")]
5945 yes: bool,
5946 },
5947 Status {
5949 #[arg(long, help = "Namespace")]
5950 namespace: Option<String>,
5951 #[arg(long, help = "Label selector (e.g. app.kubernetes.io/name=athena-operator)")]
5952 selector: Option<String>,
5953 #[arg(long, help = "Override kube context")]
5954 context: Option<String>,
5955 #[arg(long, help = "JSON output")]
5956 json: bool,
5957 },
5958}
5959
5960#[cfg(feature = "kubernetes")]
5961#[derive(Subcommand, Debug)]
5962pub enum KubernetesSubCommand {
5963 Check {
5965 #[arg(long, help = "Kubeconfig context to target")]
5966 context: Option<String>,
5967 #[arg(
5968 long,
5969 default_value = "default",
5970 help = "Namespace to probe for workload readiness"
5971 )]
5972 namespace: String,
5973 #[arg(long, help = "Skip live cluster calls (tooling check only)")]
5974 offline: bool,
5975 },
5976 Generate {
5978 #[arg(long, help = "Logical app name (used for resource names)")]
5979 name: String,
5980 #[arg(long, help = "Container image reference")]
5981 image: String,
5982 #[arg(long, default_value_t = 80, help = "Container port for the service")]
5983 port: u16,
5984 #[arg(long, default_value_t = 1, help = "Replica count")]
5985 replicas: u16,
5986 #[arg(
5987 long,
5988 default_value = "default",
5989 help = "Namespace for generated resources"
5990 )]
5991 namespace: String,
5992 #[arg(
5993 long,
5994 default_value = "k8s/xbp-manifest.yaml",
5995 help = "Path to write the manifest bundle"
5996 )]
5997 output: String,
5998 #[arg(long, help = "Optional ingress host (creates Ingress when set)")]
5999 host: Option<String>,
6000 },
6001 Render {
6003 #[arg(long, help = "Service name from services[]")]
6004 service: Option<String>,
6005 #[arg(long, help = "Deploy group name from deploy.groups")]
6006 group: Option<String>,
6007 #[arg(long, help = "Deploy environment")]
6008 env: Option<String>,
6009 #[arg(long, help = "Directory to write rendered manifests")]
6010 output: Option<PathBuf>,
6011 #[arg(long, help = "Print rendered manifests to stdout")]
6012 stdout: bool,
6013 #[arg(long, help = "JSON summary of render sources")]
6014 json: bool,
6015 #[arg(long, help = "Resolve paths only; do not invoke kustomize")]
6016 dry_run: bool,
6017 },
6018 Apply {
6020 #[arg(long, help = "Path to manifest file (legacy; prefer --service/--group)")]
6021 file: Option<String>,
6022 #[arg(long, help = "Service name from services[]")]
6023 service: Option<String>,
6024 #[arg(long, help = "Deploy group name from deploy.groups")]
6025 group: Option<String>,
6026 #[arg(long, help = "Deploy environment")]
6027 env: Option<String>,
6028 #[arg(long, help = "Override kube context")]
6029 context: Option<String>,
6030 #[arg(long, help = "Override namespace")]
6031 namespace: Option<String>,
6032 #[arg(long, help = "Use --dry-run=server")]
6033 dry_run: bool,
6034 #[arg(long, help = "Server-side apply")]
6035 server_side: bool,
6036 #[arg(long, help = "Prune resources not in the apply set")]
6037 prune: bool,
6038 #[arg(long, help = "Skip confirmation prompts")]
6039 yes: bool,
6040 },
6041 Diff {
6043 #[arg(long, help = "Service name from services[]")]
6044 service: Option<String>,
6045 #[arg(long, help = "Deploy group name from deploy.groups")]
6046 group: Option<String>,
6047 #[arg(long, help = "Deploy environment")]
6048 env: Option<String>,
6049 #[arg(long, help = "Override namespace")]
6050 namespace: Option<String>,
6051 #[arg(long, help = "Override kube context")]
6052 context: Option<String>,
6053 },
6054 Rollout {
6056 #[arg(help = "Workload (deployment/name or name shorthand)")]
6057 workload: String,
6058 #[arg(long, help = "Namespace")]
6059 namespace: Option<String>,
6060 #[arg(long, default_value = "180s", help = "Rollout timeout")]
6061 timeout: String,
6062 #[arg(long, help = "Override kube context")]
6063 context: Option<String>,
6064 #[arg(long, help = "JSON output")]
6065 json: bool,
6066 },
6067 Verify {
6069 #[arg(long, help = "Service name from services[]")]
6070 service: Option<String>,
6071 #[arg(long, help = "Deploy group name from deploy.groups")]
6072 group: Option<String>,
6073 #[arg(long, help = "Deploy environment")]
6074 env: Option<String>,
6075 #[arg(long, help = "Override namespace")]
6076 namespace: Option<String>,
6077 #[arg(long, help = "Override kube context")]
6078 context: Option<String>,
6079 #[arg(long, help = "JSON output")]
6080 json: bool,
6081 },
6082 Crds {
6084 #[command(subcommand)]
6085 command: KubernetesCrdsSubCommand,
6086 },
6087 Operator {
6089 #[command(subcommand)]
6090 command: KubernetesOperatorSubCommand,
6091 },
6092 Status {
6094 #[arg(long, default_value = "default", help = "Namespace to summarize")]
6095 namespace: String,
6096 #[arg(long, help = "Override kube context")]
6097 context: Option<String>,
6098 },
6099 Addons(KubernetesAddonCmd),
6101 DashboardToken {
6103 #[arg(
6104 long,
6105 default_value = "kube-system",
6106 help = "Namespace containing the dashboard token secret"
6107 )]
6108 namespace: String,
6109 #[arg(
6110 long,
6111 default_value = "microk8s-dashboard-token",
6112 help = "Secret name containing the dashboard login token"
6113 )]
6114 secret: String,
6115 #[arg(long, help = "Override kube context")]
6116 context: Option<String>,
6117 },
6118 ObservabilityCreds {
6120 #[arg(
6121 long,
6122 default_value = "observability",
6123 help = "Namespace containing Grafana secret"
6124 )]
6125 namespace: String,
6126 #[arg(
6127 long,
6128 default_value = "kube-prom-stack-grafana",
6129 help = "Grafana secret name"
6130 )]
6131 secret: String,
6132 #[arg(long, help = "Override kube context")]
6133 context: Option<String>,
6134 },
6135 Issuer {
6137 #[arg(
6138 long,
6139 help = "Email used for Let's Encrypt account registration (required)"
6140 )]
6141 email: String,
6142 #[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
6143 name: String,
6144 #[arg(
6145 long,
6146 default_value = "default",
6147 help = "Namespace for the Issuer resource"
6148 )]
6149 namespace: String,
6150 #[arg(
6151 long,
6152 default_value = "https://acme-v02.api.letsencrypt.org/directory",
6153 help = "ACME server URL (production by default)"
6154 )]
6155 server: String,
6156 #[arg(
6157 long,
6158 default_value = "letsencrypt-account-key",
6159 help = "Secret used to store the ACME account private key"
6160 )]
6161 private_key_secret: String,
6162 #[arg(
6163 long,
6164 default_value = "nginx",
6165 help = "Ingress class name used for HTTP01 solving"
6166 )]
6167 ingress_class_name: String,
6168 #[arg(long, help = "Override kube context")]
6169 context: Option<String>,
6170 #[arg(long, help = "Use --dry-run=server")]
6171 dry_run: bool,
6172 },
6173}
6174
6175#[cfg(test)]
6176mod tests {
6177 #[cfg(feature = "linear")]
6178 use super::LinearConfigAction;
6179 use super::{
6180 Cli, CloudflareConfigAction, CloudflareJobWorkflow, CloudflareRollout,
6181 CloudflareSubCommand, CloudflaredSubCommand, Commands, DnsProviderKind, DnsSubCommand,
6182 DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand, GenerateSubCommand,
6183 NetworkFloatingIpSubCommand, NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand,
6184 NetworkSubCommand, SshCmd, VersionSubCommand, WorktreeWatchSubCommand,
6185 };
6186 #[cfg(feature = "secrets")]
6187 use super::{
6188 CloudflareSecretsSubCommand, SecretsProviderKind, SecretsStoresSubCommand,
6189 SecretsSubCommand,
6190 };
6191 use clap::Parser;
6192 use std::path::PathBuf;
6193
6194 #[test]
6195 fn parses_network_floating_ip_add() {
6196 let cli = Cli::parse_from([
6197 "xbp",
6198 "network",
6199 "floating-ip",
6200 "add",
6201 "--ip",
6202 "1.2.3.4",
6203 "--apply",
6204 ]);
6205
6206 match cli.command {
6207 Some(Commands::Network(network)) => match network.command {
6208 NetworkSubCommand::FloatingIp(fip) => match fip.command {
6209 NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
6210 assert_eq!(ip, "1.2.3.4");
6211 assert!(apply);
6212 }
6213 _ => panic!("expected add subcommand"),
6214 },
6215 _ => panic!("expected floating-ip subcommand"),
6216 },
6217 _ => panic!("expected network command"),
6218 }
6219 }
6220
6221 #[test]
6222 fn parses_generate_config_update() {
6223 let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
6224
6225 match cli.command {
6226 Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
6227 GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
6228 _ => panic!("expected generate config command"),
6229 },
6230 _ => panic!("expected generate command"),
6231 }
6232 }
6233
6234 #[test]
6235 fn parses_commit_command_with_dry_run() {
6236 let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
6237
6238 match cli.command {
6239 Some(Commands::Commit(commit_cmd)) => {
6240 assert!(commit_cmd.dry_run);
6241 assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
6242 assert_eq!(commit_cmd.model, None);
6243 }
6244 _ => panic!("expected commit command"),
6245 }
6246 }
6247
6248 #[cfg(feature = "linear")]
6249 #[test]
6250 fn parses_linear_select_initiative_config_command() {
6251 let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
6252
6253 match cli.command {
6254 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
6255 Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
6256 assert!(matches!(
6257 linear_cmd.action,
6258 LinearConfigAction::SelectInitiative
6259 ));
6260 }
6261 _ => panic!("expected linear config provider"),
6262 },
6263 _ => panic!("expected config command"),
6264 }
6265 }
6266
6267 #[test]
6268 fn parses_ssh_command_with_cloudflared_and_key_auth() {
6269 let cli = Cli::parse_from([
6270 "xbp",
6271 "ssh",
6272 "--host",
6273 "ssh.internal",
6274 "--username",
6275 "deploy",
6276 "--private-key",
6277 "C:/Users/floris/.ssh/id_ed25519",
6278 "--cloudflared-hostname",
6279 "bastion.example.com",
6280 "--command",
6281 "htop",
6282 ]);
6283
6284 let Some(Commands::Ssh(SshCmd {
6285 ssh_host,
6286 ssh_username,
6287 private_key,
6288 cloudflared_hostname,
6289 command,
6290 ..
6291 })) = cli.command
6292 else {
6293 panic!("expected shell command");
6294 };
6295
6296 assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
6297 assert_eq!(ssh_username.as_deref(), Some("deploy"));
6298 assert_eq!(
6299 private_key,
6300 Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
6301 );
6302 assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
6303 assert_eq!(command.as_deref(), Some("htop"));
6304 }
6305
6306 #[test]
6307 fn parses_cloudflared_tcp_command() {
6308 let cli = Cli::parse_from([
6309 "xbp",
6310 "cloudflared",
6311 "tcp",
6312 "--hostname",
6313 "bastion.example.com",
6314 "--listener",
6315 "127.0.0.1:2222",
6316 ]);
6317
6318 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
6319 panic!("expected cloudflared command");
6320 };
6321
6322 match cloudflared_cmd.command {
6323 CloudflaredSubCommand::Tcp(tcp_cmd) => {
6324 assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
6325 assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
6326 }
6327 }
6328 }
6329
6330 #[test]
6331 fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
6332 let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
6333
6334 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
6335 panic!("expected cloudflared command");
6336 };
6337
6338 match cloudflared_cmd.command {
6339 CloudflaredSubCommand::Tcp(tcp_cmd) => {
6340 assert_eq!(tcp_cmd.hostname, None);
6341 assert_eq!(tcp_cmd.listener, None);
6342 }
6343 }
6344 }
6345
6346 #[test]
6347 fn parses_cloudflare_doctor_with_app_after_subcommand() {
6348 let cli = Cli::parse_from(["xbp", "cloudflare", "doctor", "--app", "auth"]);
6349
6350 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6351 panic!("expected cloudflare command");
6352 };
6353 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
6354 assert!(matches!(
6355 cloudflare_cmd.command,
6356 CloudflareSubCommand::Doctor(_)
6357 ));
6358 }
6359
6360 #[test]
6361 fn parses_cloudflare_tunnel_run_with_token_and_log_level() {
6362 let cli = Cli::parse_from([
6363 "xbp",
6364 "cloudflare",
6365 "tunnel",
6366 "run",
6367 "--token",
6368 "tunnel-token",
6369 "--log-level",
6370 "debug",
6371 ]);
6372
6373 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6374 panic!("expected cloudflare command");
6375 };
6376
6377 match cloudflare_cmd.command {
6378 CloudflareSubCommand::Tunnel(tunnel_cmd) => match tunnel_cmd.command {
6379 super::CloudflareTunnelSubCommand::Run(run_cmd) => {
6380 assert_eq!(run_cmd.tunnel, None);
6381 assert_eq!(run_cmd.token.as_deref(), Some("tunnel-token"));
6382 assert_eq!(run_cmd.log_level.as_deref(), Some("debug"));
6383 }
6384 _ => panic!("expected tunnel run command"),
6385 },
6386 _ => panic!("expected cloudflare tunnel command"),
6387 }
6388 }
6389
6390 #[test]
6391 fn parses_cloudflare_workflows_instance_command_with_local_flags() {
6392 let cli = Cli::parse_from([
6393 "xbp",
6394 "cloudflare",
6395 "workflows",
6396 "instances",
6397 "send-event",
6398 "my-workflow",
6399 "latest",
6400 "--type",
6401 "my-event",
6402 "--payload",
6403 "{\"key\":\"value\"}",
6404 "--local",
6405 "--port",
6406 "8787",
6407 ]);
6408
6409 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6410 panic!("expected cloudflare command");
6411 };
6412
6413 match cloudflare_cmd.command {
6414 CloudflareSubCommand::Workflows(workflows_cmd) => match workflows_cmd.command {
6415 super::CloudflareWorkflowsSubCommand::External(args) => assert_eq!(
6416 args,
6417 [
6418 "instances",
6419 "send-event",
6420 "my-workflow",
6421 "latest",
6422 "--type",
6423 "my-event",
6424 "--payload",
6425 "{\"key\":\"value\"}",
6426 "--local",
6427 "--port",
6428 "8787"
6429 ]
6430 ),
6431 },
6432 _ => panic!("expected cloudflare workflows command"),
6433 }
6434 }
6435
6436 #[test]
6437 fn parses_cloudflare_env_dev_with_environment_and_port() {
6438 let cli = Cli::parse_from([
6439 "xbp",
6440 "cloudflare",
6441 "env",
6442 "dev",
6443 "--env",
6444 "staging",
6445 "--port",
6446 "8787",
6447 ]);
6448
6449 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6450 panic!("expected cloudflare command");
6451 };
6452
6453 match cloudflare_cmd.command {
6454 CloudflareSubCommand::Env(env_cmd) => match env_cmd.command {
6455 super::CloudflareEnvSubCommand::External(args) => {
6456 assert_eq!(args, ["dev", "--env", "staging", "--port", "8787"])
6457 }
6458 },
6459 _ => panic!("expected cloudflare env command"),
6460 }
6461 }
6462
6463 #[test]
6464 fn parses_cloudflare_release_with_domain_guard() {
6465 let cli = Cli::parse_from([
6466 "xbp",
6467 "cloudflare",
6468 "release",
6469 "--app",
6470 "auth",
6471 "--version",
6472 "1.2.3",
6473 "--rollout",
6474 "gradual",
6475 "--domain",
6476 "auth-runtime",
6477 ]);
6478
6479 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6480 panic!("expected cloudflare command");
6481 };
6482 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
6483 match cloudflare_cmd.command {
6484 CloudflareSubCommand::Release(release_cmd) => {
6485 assert_eq!(release_cmd.version, "1.2.3");
6486 assert_eq!(release_cmd.rollout, CloudflareRollout::Gradual);
6487 assert_eq!(release_cmd.domain.as_deref(), Some("auth-runtime"));
6488 }
6489 _ => panic!("expected release subcommand"),
6490 }
6491 }
6492
6493 #[test]
6494 fn parses_cloudflare_deploy_parity_flags() {
6495 let cli = Cli::parse_from([
6496 "xbp",
6497 "cloudflare",
6498 "deploy",
6499 "--app",
6500 "auth",
6501 "--rollout",
6502 "none",
6503 "--skip-deploy",
6504 "--allow-unchanged-container-image",
6505 "--prune-old-images",
6506 "--keep-image-tag-count",
6507 "7",
6508 ]);
6509
6510 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6511 panic!("expected cloudflare command");
6512 };
6513 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
6514 match cloudflare_cmd.command {
6515 CloudflareSubCommand::Deploy(deploy_cmd) => {
6516 assert_eq!(deploy_cmd.rollout, CloudflareRollout::None);
6517 assert!(deploy_cmd.skip_deploy);
6518 assert!(deploy_cmd.allow_unchanged_container_image);
6519 assert!(deploy_cmd.prune_old_images);
6520 assert_eq!(deploy_cmd.keep_image_tag_count, Some(7));
6521 }
6522 _ => panic!("expected deploy subcommand"),
6523 }
6524 }
6525
6526 #[test]
6527 fn parses_cloudflare_release_parity_flags() {
6528 let cli = Cli::parse_from([
6529 "xbp",
6530 "cloudflare",
6531 "release",
6532 "--app",
6533 "auth",
6534 "--version",
6535 "1.2.3",
6536 "--rollout",
6537 "none",
6538 "--skip-deploy",
6539 "--allow-unchanged-container-image",
6540 "--prune-old-images",
6541 "--keep-image-tag-count",
6542 "5",
6543 ]);
6544
6545 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6546 panic!("expected cloudflare command");
6547 };
6548 match cloudflare_cmd.command {
6549 CloudflareSubCommand::Release(release_cmd) => {
6550 assert_eq!(release_cmd.rollout, CloudflareRollout::None);
6551 assert!(release_cmd.skip_deploy);
6552 assert!(release_cmd.allow_unchanged_container_image);
6553 assert!(release_cmd.prune_old_images);
6554 assert_eq!(release_cmd.keep_image_tag_count, Some(5));
6555 }
6556 _ => panic!("expected release subcommand"),
6557 }
6558 }
6559
6560 #[test]
6561 fn parses_cloudflare_jobs_enqueue_with_workflow_payload() {
6562 let cli = Cli::parse_from([
6563 "xbp",
6564 "cloudflare",
6565 "jobs",
6566 "enqueue",
6567 "release",
6568 "--app",
6569 "auth",
6570 "--version",
6571 "1.2.3",
6572 "--rollout",
6573 "gradual",
6574 "--skip-deploy",
6575 "--prune-old-images",
6576 ]);
6577
6578 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
6579 panic!("expected cloudflare command");
6580 };
6581 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
6582 match cloudflare_cmd.command {
6583 CloudflareSubCommand::Jobs(jobs_cmd) => match jobs_cmd.command {
6584 super::CloudflareJobsSubCommand::Enqueue(enqueue_cmd) => {
6585 assert_eq!(enqueue_cmd.workflow, CloudflareJobWorkflow::Release);
6586 assert_eq!(enqueue_cmd.version.as_deref(), Some("1.2.3"));
6587 assert_eq!(enqueue_cmd.rollout, CloudflareRollout::Gradual);
6588 assert!(enqueue_cmd.skip_deploy);
6589 assert!(enqueue_cmd.prune_old_images);
6590 }
6591 _ => panic!("expected enqueue subcommand"),
6592 },
6593 _ => panic!("expected jobs subcommand"),
6594 }
6595 }
6596
6597 #[test]
6598 fn parses_worktree_watch_stop_force_command() {
6599 let cli = Cli::parse_from([
6600 "xbp",
6601 "worktree-watch",
6602 "stop",
6603 "--repo",
6604 "C:/Users/floris/Documents/GitHub/xbp",
6605 "--force",
6606 ]);
6607
6608 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
6609 panic!("expected worktree-watch command");
6610 };
6611
6612 match worktree_cmd.command {
6613 WorktreeWatchSubCommand::Stop(stop_cmd) => {
6614 assert_eq!(
6615 stop_cmd.target.repo,
6616 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/xbp"))
6617 );
6618 assert!(stop_cmd.force);
6619 }
6620 _ => panic!("expected stop subcommand"),
6621 }
6622 }
6623
6624 #[test]
6625 fn parses_worktree_watch_parent_detach_command() {
6626 let cli = Cli::parse_from([
6627 "xbp",
6628 "worktree-watch",
6629 "start",
6630 "--parent",
6631 "C:/Users/floris/Documents/GitHub",
6632 "--detach",
6633 ]);
6634
6635 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
6636 panic!("expected worktree-watch command");
6637 };
6638
6639 match worktree_cmd.command {
6640 WorktreeWatchSubCommand::Start(start_cmd) => {
6641 assert_eq!(
6642 start_cmd.target.parent,
6643 Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
6644 );
6645 assert!(start_cmd.detach);
6646 }
6647 _ => panic!("expected start subcommand"),
6648 }
6649 }
6650
6651 #[test]
6652 fn parses_worktree_watch_status_repo_activity_command() {
6653 let cli = Cli::parse_from([
6654 "xbp",
6655 "worktree-watch",
6656 "status",
6657 "--repo-activity",
6658 "--stats-gap-minutes",
6659 "30",
6660 ]);
6661
6662 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
6663 panic!("expected worktree-watch command");
6664 };
6665
6666 match worktree_cmd.command {
6667 WorktreeWatchSubCommand::Status(status_cmd) => {
6668 assert!(status_cmd.repo_activity);
6669 assert_eq!(status_cmd.stats_gap_minutes, 30);
6670 }
6671 _ => panic!("expected status subcommand"),
6672 }
6673 }
6674
6675 #[test]
6676 fn parses_worktree_watch_tray_parent_command() {
6677 let cli = Cli::parse_from([
6678 "xbp",
6679 "worktree-watch",
6680 "tray",
6681 "--parent",
6682 "C:/Users/floris/Documents/GitHub",
6683 "--sync-interval-seconds",
6684 "90",
6685 ]);
6686
6687 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
6688 panic!("expected worktree-watch command");
6689 };
6690
6691 match worktree_cmd.command {
6692 WorktreeWatchSubCommand::Tray(tray_cmd) => {
6693 assert_eq!(
6694 tray_cmd.target.parent,
6695 Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
6696 );
6697 assert!(tray_cmd.target.repos.is_empty());
6698 assert_eq!(tray_cmd.sync_interval_seconds, 90);
6699 }
6700 _ => panic!("expected tray subcommand"),
6701 }
6702 }
6703
6704 #[test]
6705 fn parses_worktree_watch_tray_foreground_command() {
6706 let cli = Cli::parse_from([
6707 "xbp",
6708 "worktree-watch",
6709 "tray",
6710 "--foreground",
6711 "--parent",
6712 "C:/Users/floris/Documents/GitHub",
6713 ]);
6714
6715 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
6716 panic!("expected worktree-watch command");
6717 };
6718
6719 match worktree_cmd.command {
6720 WorktreeWatchSubCommand::Tray(tray_cmd) => {
6721 assert!(tray_cmd.foreground);
6722 assert_eq!(
6723 tray_cmd.target.parent,
6724 Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
6725 );
6726 }
6727 _ => panic!("expected tray subcommand"),
6728 }
6729 }
6730
6731 #[test]
6732 fn parses_worktree_watch_tray_repos_command() {
6733 let cli = Cli::parse_from([
6734 "xbp",
6735 "worktree-watch",
6736 "tray",
6737 "--repos",
6738 "C:/src/a",
6739 "C:/src/b",
6740 ]);
6741
6742 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
6743 panic!("expected worktree-watch command");
6744 };
6745
6746 match worktree_cmd.command {
6747 WorktreeWatchSubCommand::Tray(tray_cmd) => {
6748 assert_eq!(
6749 tray_cmd.target.repos,
6750 vec![PathBuf::from("C:/src/a"), PathBuf::from("C:/src/b")]
6751 );
6752 }
6753 _ => panic!("expected tray subcommand"),
6754 }
6755 }
6756
6757 #[test]
6758 fn parses_version_workspace_publish_run_command() {
6759 let cli = Cli::parse_from([
6760 "xbp",
6761 "version",
6762 "workspace",
6763 "publish",
6764 "run",
6765 "--repo",
6766 "C:/Users/floris/Documents/GitHub/athena",
6767 "--dry-run",
6768 "--from",
6769 "athena-s3",
6770 ]);
6771
6772 let Some(Commands::Version(version_cmd)) = cli.command else {
6773 panic!("expected version command");
6774 };
6775
6776 match version_cmd.command {
6777 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
6778 match workspace_cmd.command {
6779 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
6780 match publish_cmd.command {
6781 super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
6782 assert_eq!(
6783 run_cmd.target.repo,
6784 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
6785 );
6786 assert!(!run_cmd.target.json);
6787 assert!(run_cmd.dry_run);
6788 assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
6789 assert!(run_cmd.auto_fix);
6790 }
6791 _ => panic!("expected publish run"),
6792 }
6793 }
6794 _ => panic!("expected workspace publish"),
6795 }
6796 }
6797 _ => panic!("expected version workspace command"),
6798 }
6799 }
6800
6801 #[test]
6802 fn parses_version_workspace_publish_plan_with_only_and_include_prereqs() {
6803 let cli = Cli::parse_from([
6804 "xbp",
6805 "version",
6806 "workspace",
6807 "publish",
6808 "plan",
6809 "--repo",
6810 "C:/Users/floris/Documents/GitHub/athena-auth",
6811 "--only",
6812 "athena-auth",
6813 "--include-prereqs",
6814 ]);
6815
6816 let Some(Commands::Version(version_cmd)) = cli.command else {
6817 panic!("expected version command");
6818 };
6819
6820 match version_cmd.command {
6821 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
6822 match workspace_cmd.command {
6823 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
6824 match publish_cmd.command {
6825 super::VersionWorkspacePublishSubCommand::Plan(plan_cmd) => {
6826 assert_eq!(
6827 plan_cmd.target.repo,
6828 Some(PathBuf::from(
6829 "C:/Users/floris/Documents/GitHub/athena-auth"
6830 ))
6831 );
6832 assert_eq!(plan_cmd.only.as_deref(), Some("athena-auth"));
6833 assert!(plan_cmd.include_prereqs);
6834 }
6835 _ => panic!("expected publish plan"),
6836 }
6837 }
6838 _ => panic!("expected workspace publish"),
6839 }
6840 }
6841 _ => panic!("expected version workspace command"),
6842 }
6843 }
6844
6845 #[test]
6846 fn parses_commit_alias_with_push_flag() {
6847 let cli = Cli::parse_from(["xbp", "c", "-p"]);
6848
6849 let Some(Commands::Commit(commit_cmd)) = cli.command else {
6850 panic!("expected commit command");
6851 };
6852
6853 assert!(commit_cmd.push);
6854 assert!(!commit_cmd.dry_run);
6855 }
6856
6857 #[test]
6858 fn parses_global_push_flag() {
6859 let cli = Cli::parse_from(["xbp", "--push", "version", "patch"]);
6860 assert!(cli.push);
6861
6862 let Some(Commands::Version(version_cmd)) = cli.command else {
6863 panic!("expected version command");
6864 };
6865 assert_eq!(version_cmd.target.as_deref(), Some("patch"));
6866 }
6867
6868 #[test]
6869 fn parses_version_push_flag() {
6870 let cli = Cli::parse_from(["xbp", "version", "patch", "--push"]);
6871 assert!(cli.push);
6872
6873 let Some(Commands::Version(version_cmd)) = cli.command else {
6874 panic!("expected version command");
6875 };
6876 assert!(version_cmd.push);
6877 }
6878
6879 #[test]
6880 fn parses_version_bump_auto_plan_and_alias() {
6881 let cli = Cli::parse_from([
6882 "xbp",
6883 "v",
6884 "b",
6885 "--auto",
6886 "--plan",
6887 "--include-unchanged",
6888 "--no-release-prompt",
6889 ]);
6890
6891 let Some(Commands::Version(version_cmd)) = cli.command else {
6892 panic!("expected version command");
6893 };
6894 let Some(VersionSubCommand::Bump(bump)) = version_cmd.command else {
6895 panic!("expected bump subcommand");
6896 };
6897 assert!(bump.auto);
6898 assert!(bump.plan);
6899 assert!(bump.include_unchanged);
6900 assert!(bump.no_release_prompt);
6901 assert!(!bump.release);
6902 }
6903
6904 #[test]
6905 fn parses_version_bump_push_flag() {
6906 let cli = Cli::parse_from(["xbp", "version", "bump", "--all", "--patch", "-p"]);
6907
6908 let Some(Commands::Version(version_cmd)) = cli.command else {
6909 panic!("expected version command");
6910 };
6911 let Some(VersionSubCommand::Bump(bump_cmd)) = version_cmd.command else {
6912 panic!("expected version bump subcommand");
6913 };
6914
6915 assert!(bump_cmd.push);
6916 assert!(bump_cmd.all);
6917 assert!(bump_cmd.patch);
6918 }
6919
6920 #[test]
6921 fn parses_version_alias_release_alias() {
6922 let cli = Cli::parse_from([
6923 "xbp",
6924 "v",
6925 "r",
6926 "--draft",
6927 "--publish",
6928 "--force",
6929 "--flag",
6930 "nightly",
6931 ]);
6932
6933 let Some(Commands::Version(version_cmd)) = cli.command else {
6934 panic!("expected version command");
6935 };
6936
6937 let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
6938 panic!("expected release subcommand");
6939 };
6940
6941 assert!(release_cmd.draft);
6942 assert!(release_cmd.publish);
6943 assert!(release_cmd.force);
6944 assert_eq!(release_cmd.flag, Some(super::VersionReleaseFlag::Nightly));
6945 }
6946
6947 #[test]
6948 fn parses_version_domain_release_guard_flags() {
6949 let cli = Cli::parse_from([
6950 "xbp",
6951 "version",
6952 "domain",
6953 "release",
6954 "--domain",
6955 "auth-runtime",
6956 "--version",
6957 "2.0.0",
6958 "--deploy",
6959 "--allow-major-jump",
6960 "--allow-cross-domain-version",
6961 "platform",
6962 ]);
6963
6964 let Some(Commands::Version(version_cmd)) = cli.command else {
6965 panic!("expected version command");
6966 };
6967
6968 let Some(super::VersionSubCommand::Domain(domain_cmd)) = version_cmd.command else {
6969 panic!("expected domain command");
6970 };
6971
6972 match domain_cmd.command {
6973 super::VersionDomainSubCommand::Release(release_cmd) => {
6974 assert_eq!(release_cmd.domain, "auth-runtime");
6975 assert_eq!(release_cmd.version.as_deref(), Some("2.0.0"));
6976 assert!(release_cmd.deploy);
6977 assert!(release_cmd.allow_major_jump);
6978 assert_eq!(
6979 release_cmd.allow_cross_domain_version.as_deref(),
6980 Some("platform")
6981 );
6982 }
6983 _ => panic!("expected domain release"),
6984 }
6985 }
6986
6987 #[test]
6988 fn parses_publish_command_target_filter() {
6989 let cli = Cli::parse_from([
6990 "xbp",
6991 "publish",
6992 "--allow-dirty",
6993 "--force",
6994 "--include-prereqs",
6995 "--target",
6996 "npm",
6997 "--service",
6998 "web",
6999 "--manifest-path",
7000 "apps/web/package.json",
7001 ]);
7002
7003 let Some(Commands::Publish(publish_cmd)) = cli.command else {
7004 panic!("expected publish command");
7005 };
7006
7007 assert!(publish_cmd.allow_dirty);
7008 assert!(publish_cmd.force);
7009 assert!(publish_cmd.include_prereqs);
7010 assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
7011 assert_eq!(publish_cmd.service.as_deref(), Some("web"));
7012 assert_eq!(
7013 publish_cmd.manifest_path,
7014 Some(PathBuf::from("apps/web/package.json"))
7015 );
7016 }
7017
7018 #[test]
7019 fn parses_npm_setup_release_config_command() {
7020 let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
7021
7022 let Some(Commands::Config(config_cmd)) = cli.command else {
7023 panic!("expected config command");
7024 };
7025 let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
7026 panic!("expected npm config command");
7027 };
7028
7029 assert!(matches!(
7030 registry_cmd.action,
7031 super::RegistryConfigAction::SetupRelease
7032 ));
7033 }
7034
7035 #[test]
7036 fn parses_release_setup_config_command() {
7037 let cli = Cli::parse_from(["xbp", "config", "release", "setup"]);
7038
7039 let Some(Commands::Config(config_cmd)) = cli.command else {
7040 panic!("expected config command");
7041 };
7042 let Some(super::ConfigProviderCmd::Release(release_cmd)) = config_cmd.provider else {
7043 panic!("expected release config command");
7044 };
7045
7046 assert!(matches!(
7047 release_cmd.action,
7048 super::ReleaseConfigAction::Setup
7049 ));
7050 }
7051
7052 #[test]
7053 fn parses_crates_login_config_command() {
7054 let cli = Cli::parse_from(["xbp", "config", "crates", "login"]);
7055
7056 let Some(Commands::Config(config_cmd)) = cli.command else {
7057 panic!("expected config command");
7058 };
7059 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
7060 panic!("expected crates config command");
7061 };
7062
7063 assert!(matches!(
7064 crates_cmd.action,
7065 super::CratesConfigAction::Login { .. }
7066 ));
7067 }
7068
7069 #[test]
7070 fn parses_crates_logout_config_command() {
7071 let cli = Cli::parse_from(["xbp", "config", "crates", "logout"]);
7072
7073 let Some(Commands::Config(config_cmd)) = cli.command else {
7074 panic!("expected config command");
7075 };
7076 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
7077 panic!("expected crates config command");
7078 };
7079
7080 assert!(matches!(
7081 crates_cmd.action,
7082 super::CratesConfigAction::Logout
7083 ));
7084 }
7085
7086 #[test]
7087 fn parses_whoami_command() {
7088 let cli = Cli::parse_from(["xbp", "whoami"]);
7089
7090 assert!(matches!(cli.command, Some(Commands::Whoami)));
7091 }
7092
7093 #[test]
7094 fn parses_update_command_with_flags() {
7095 let cli = Cli::parse_from([
7096 "xbp",
7097 "update",
7098 "--json",
7099 "--install",
7100 "--fail-if-outdated",
7101 "--crate",
7102 "xbp",
7103 "--features",
7104 "secrets,docker",
7105 "--no-default-features",
7106 ]);
7107
7108 let Some(Commands::Update(cmd)) = cli.command else {
7109 panic!("expected update command");
7110 };
7111 assert!(cmd.json);
7112 assert!(cmd.install);
7113 assert!(cmd.fail_if_outdated);
7114 assert_eq!(cmd.crate_name, "xbp");
7115 assert_eq!(cmd.features, vec!["secrets".to_string(), "docker".to_string()]);
7116 assert!(cmd.no_default_features);
7117 assert!(!cmd.all_features);
7118 }
7119
7120 #[test]
7121 fn parses_update_install_all_features_flag() {
7122 let cli = Cli::parse_from(["xbp", "update", "--install", "--all-features"]);
7123 let Some(Commands::Update(cmd)) = cli.command else {
7124 panic!("expected update command");
7125 };
7126 assert!(cmd.install);
7127 assert!(cmd.all_features);
7128 assert!(cmd.features.is_empty());
7129 }
7130
7131 #[test]
7132 fn parses_upgrade_alias_as_update() {
7133 let cli = Cli::parse_from(["xbp", "upgrade"]);
7134 assert!(matches!(cli.command, Some(Commands::Update(_))));
7135 }
7136
7137 #[test]
7138 fn parses_shell_alias_as_ssh_command() {
7139 let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
7140
7141 let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
7142 panic!("expected ssh command through shell alias");
7143 };
7144
7145 assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
7146 }
7147
7148 #[test]
7149 fn parses_api_request_command() {
7150 let cli = Cli::parse_from([
7151 "xbp",
7152 "api",
7153 "request",
7154 "/api/registry/installers/python-pip",
7155 "--web",
7156 "--method",
7157 "GET",
7158 "--header",
7159 "accept: application/json",
7160 ]);
7161
7162 let Some(Commands::Api(api_cmd)) = cli.command else {
7163 panic!("expected api command");
7164 };
7165
7166 match api_cmd.command {
7167 super::ApiSubCommand::Request(request_cmd) => {
7168 assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
7169 assert!(request_cmd.target.web);
7170 assert_eq!(request_cmd.method.as_deref(), Some("GET"));
7171 assert_eq!(
7172 request_cmd.target.header,
7173 vec!["accept: application/json".to_string()]
7174 );
7175 }
7176 _ => panic!("expected api request subcommand"),
7177 }
7178 }
7179
7180 #[test]
7181 fn parses_api_projects_list_command() {
7182 let cli = Cli::parse_from([
7183 "xbp",
7184 "api",
7185 "projects",
7186 "list",
7187 "--organization-id",
7188 "org_123",
7189 ]);
7190
7191 let Some(Commands::Api(api_cmd)) = cli.command else {
7192 panic!("expected api command");
7193 };
7194
7195 match api_cmd.command {
7196 super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
7197 super::ApiProjectsSubCommand::List(list_cmd) => {
7198 assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
7199 }
7200 _ => panic!("expected projects list subcommand"),
7201 },
7202 _ => panic!("expected projects subcommand"),
7203 }
7204 }
7205
7206 #[test]
7207 fn parses_api_routes_create_command() {
7208 let cli = Cli::parse_from([
7209 "xbp",
7210 "api",
7211 "routes",
7212 "create",
7213 "--domain",
7214 "demo.local",
7215 "--target",
7216 "http://127.0.0.1:3000",
7217 "--weighted-target",
7218 "http://127.0.0.1:3001=3",
7219 "--base-url",
7220 "http://127.0.0.1:8080",
7221 ]);
7222
7223 let Some(Commands::Api(api_cmd)) = cli.command else {
7224 panic!("expected api command");
7225 };
7226
7227 match api_cmd.command {
7228 super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
7229 super::ApiRoutesSubCommand::Create(create_cmd) => {
7230 assert_eq!(create_cmd.domain, "demo.local");
7231 assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
7232 assert_eq!(
7233 create_cmd.weighted_target,
7234 vec!["http://127.0.0.1:3001=3".to_string()]
7235 );
7236 assert_eq!(
7237 create_cmd.target_options.base_url.as_deref(),
7238 Some("http://127.0.0.1:8080")
7239 );
7240 }
7241 _ => panic!("expected routes create subcommand"),
7242 },
7243 _ => panic!("expected routes subcommand"),
7244 }
7245 }
7246
7247 #[test]
7248 fn parses_hetzner_vswitch_setup_command() {
7249 let cli = Cli::parse_from([
7250 "xbp",
7251 "network",
7252 "hetzner",
7253 "vswitch",
7254 "setup",
7255 "--ip",
7256 "10.0.3.2",
7257 "--vlan-id",
7258 "4000",
7259 "--interface",
7260 "enp0s31f6",
7261 "--apply",
7262 ]);
7263
7264 let Some(Commands::Network(network_cmd)) = cli.command else {
7265 panic!("expected network command");
7266 };
7267
7268 match network_cmd.command {
7269 NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
7270 NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
7271 NetworkHetznerVswitchSubCommand::Setup {
7272 ip,
7273 cidr,
7274 interface,
7275 vlan_id,
7276 apply,
7277 ..
7278 } => {
7279 assert_eq!(ip, "10.0.3.2");
7280 assert_eq!(cidr, 24);
7281 assert_eq!(interface.as_deref(), Some("enp0s31f6"));
7282 assert_eq!(vlan_id, 4000);
7283 assert!(apply);
7284 }
7285 },
7286 },
7287 _ => panic!("expected hetzner subcommand"),
7288 }
7289 }
7290
7291 #[cfg(feature = "secrets")]
7292 #[test]
7293 fn parses_secrets_diag_command() {
7294 let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
7295
7296 match cli.command {
7297 Some(Commands::Secrets(secrets_cmd)) => {
7298 assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
7299 assert_eq!(secrets_cmd.environment, "xbp-dev");
7300 }
7301 _ => panic!("expected secrets command"),
7302 }
7303 }
7304
7305 #[cfg(feature = "secrets")]
7306 #[test]
7307 fn parses_secrets_environment_override() {
7308 let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
7309
7310 match cli.command {
7311 Some(Commands::Secrets(secrets_cmd)) => {
7312 assert_eq!(secrets_cmd.environment, "xbp-prod");
7313 assert!(matches!(
7314 secrets_cmd.command,
7315 Some(SecretsSubCommand::Push(_))
7316 ));
7317 }
7318 _ => panic!("expected secrets command"),
7319 }
7320 }
7321
7322 #[cfg(feature = "secrets")]
7323 #[test]
7324 fn parses_secrets_dev_vars_sync_flags() {
7325 let cli = Cli::parse_from([
7326 "xbp",
7327 "secrets",
7328 "--environment",
7329 "xbp-prod",
7330 "pull",
7331 "--include-dev-vars",
7332 "--dev-vars-output",
7333 ".dev.vars",
7334 ]);
7335
7336 match cli.command {
7337 Some(Commands::Secrets(secrets_cmd)) => {
7338 assert_eq!(secrets_cmd.environment, "xbp-prod");
7339 match secrets_cmd.command {
7340 Some(SecretsSubCommand::Pull(pull_cmd)) => {
7341 assert!(pull_cmd.include_dev_vars);
7342 assert_eq!(pull_cmd.dev_vars_output.as_deref(), Some(".dev.vars"));
7343 }
7344 _ => panic!("expected pull command"),
7345 }
7346 }
7347 _ => panic!("expected secrets command"),
7348 }
7349 }
7350
7351 #[test]
7352 fn parses_version_discover_command() {
7353 let cli = Cli::parse_from(["xbp", "version", "discover", "--dry-run"]);
7354
7355 match cli.command {
7356 Some(Commands::Version(version_cmd)) => match version_cmd.command {
7357 Some(super::VersionSubCommand::Discover(discover_cmd)) => {
7358 assert!(discover_cmd.dry_run);
7359 assert!(!discover_cmd.no_register);
7360 }
7361 _ => panic!("expected version discover subcommand"),
7362 },
7363 _ => panic!("expected version command"),
7364 }
7365 }
7366
7367 #[cfg(feature = "secrets")]
7368 #[test]
7369 fn parses_secrets_providers_command() {
7370 let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
7371
7372 match cli.command {
7373 Some(Commands::Secrets(secrets_cmd)) => {
7374 assert!(matches!(
7375 secrets_cmd.command,
7376 Some(SecretsSubCommand::Providers)
7377 ));
7378 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
7379 }
7380 _ => panic!("expected secrets command"),
7381 }
7382 }
7383
7384 #[cfg(feature = "secrets")]
7385 #[test]
7386 fn parses_cloudflare_secret_store_create() {
7387 let cli = Cli::parse_from([
7388 "xbp",
7389 "secrets",
7390 "--provider",
7391 "cloudflare",
7392 "stores",
7393 "create",
7394 "--name",
7395 "prod",
7396 ]);
7397
7398 match cli.command {
7399 Some(Commands::Secrets(secrets_cmd)) => {
7400 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
7401 match secrets_cmd.command {
7402 Some(SecretsSubCommand::Stores(stores_cmd)) => {
7403 assert!(matches!(
7404 stores_cmd.command,
7405 SecretsStoresSubCommand::Create(_)
7406 ));
7407 }
7408 _ => panic!("expected stores subcommand"),
7409 }
7410 }
7411 _ => panic!("expected secrets command"),
7412 }
7413 }
7414
7415 #[cfg(feature = "secrets")]
7416 #[test]
7417 fn parses_cloudflare_secret_duplicate() {
7418 let cli = Cli::parse_from([
7419 "xbp",
7420 "secrets",
7421 "--provider",
7422 "cloudflare",
7423 "secrets",
7424 "duplicate",
7425 "--store-id",
7426 "store_1",
7427 "--secret-id",
7428 "secret_1",
7429 "--name",
7430 "COPY",
7431 ]);
7432
7433 match cli.command {
7434 Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
7435 Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
7436 assert!(matches!(
7437 secrets_cmd.command,
7438 CloudflareSecretsSubCommand::Duplicate(_)
7439 ));
7440 }
7441 _ => panic!("expected cloudflare secrets subcommand"),
7442 },
7443 _ => panic!("expected secrets command"),
7444 }
7445 }
7446
7447 #[test]
7448 fn parses_workers_secret_put_from_stdin_command() {
7449 let cli = Cli::parse_from([
7450 "xbp",
7451 "workers",
7452 "secrets",
7453 "--environment",
7454 "production",
7455 "put",
7456 "--name",
7457 "API_KEY",
7458 "--from-stdin",
7459 ]);
7460
7461 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7462 panic!("expected workers command");
7463 };
7464
7465 match workers_cmd.command {
7466 super::WorkersSubCommand::Secrets(secrets_cmd) => {
7467 assert_eq!(
7468 secrets_cmd.target.environment.as_deref(),
7469 Some("production")
7470 );
7471 match secrets_cmd.command {
7472 super::WorkersSecretsSubCommand::Put(put_cmd) => {
7473 assert_eq!(put_cmd.name, "API_KEY");
7474 assert!(put_cmd.from_stdin);
7475 assert_eq!(put_cmd.value, None);
7476 }
7477 _ => panic!("expected workers secret put"),
7478 }
7479 }
7480 _ => panic!("expected workers secrets command"),
7481 }
7482 }
7483
7484 #[test]
7485 fn parses_workers_d1_migrations_local_command() {
7486 let cli = Cli::parse_from([
7487 "xbp",
7488 "workers",
7489 "d1",
7490 "migrations",
7491 "apply",
7492 "DB",
7493 "--local",
7494 "--environment",
7495 "preview",
7496 ]);
7497
7498 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7499 panic!("expected workers command");
7500 };
7501
7502 match workers_cmd.command {
7503 super::WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
7504 super::WorkersD1SubCommand::Migrations(migrations_cmd) => {
7505 match migrations_cmd.command {
7506 super::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
7507 assert_eq!(apply_cmd.database, "DB");
7508 assert!(apply_cmd.local);
7509 assert!(!apply_cmd.remote);
7510 assert_eq!(apply_cmd.target.environment.as_deref(), Some("preview"));
7511 }
7512 }
7513 }
7514 },
7515 _ => panic!("expected workers d1 command"),
7516 }
7517 }
7518
7519 #[test]
7520 fn parses_workers_wrangler_passthrough_for_r2_queues_and_secrets_store() {
7521 let cli = Cli::parse_from([
7522 "xbp",
7523 "workers",
7524 "wrangler",
7525 "run",
7526 "--",
7527 "r2",
7528 "bucket",
7529 "cors",
7530 "set",
7531 "r2-bucket",
7532 "--file",
7533 "cors.json",
7534 ]);
7535
7536 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7537 panic!("expected workers command");
7538 };
7539
7540 match workers_cmd.command {
7541 super::WorkersSubCommand::Wrangler(wrangler_cmd) => match wrangler_cmd.command {
7542 super::WorkersWranglerSubCommand::Run(run_cmd) => assert_eq!(
7543 run_cmd.args,
7544 [
7545 "r2",
7546 "bucket",
7547 "cors",
7548 "set",
7549 "r2-bucket",
7550 "--file",
7551 "cors.json"
7552 ]
7553 ),
7554 _ => panic!("expected Wrangler run command"),
7555 },
7556 _ => panic!("expected workers wrangler command"),
7557 }
7558 }
7559
7560 #[test]
7561 fn parses_workers_deploy_ci_version_upload_command() {
7562 let cli = Cli::parse_from(["xbp", "workers", "deploy", "ci", "--version-upload"]);
7563
7564 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7565 panic!("expected workers command");
7566 };
7567
7568 match workers_cmd.command {
7569 super::WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
7570 super::WorkersDeploySubCommand::Ci(ci_cmd) => {
7571 assert!(ci_cmd.version_upload);
7572 }
7573 _ => panic!("expected workers deploy ci command"),
7574 },
7575 _ => panic!("expected workers deploy command"),
7576 }
7577 }
7578
7579 #[test]
7580 fn parses_workers_list_alias_command() {
7581 let cli = Cli::parse_from(["xbp", "workers", "ls", "--all"]);
7582
7583 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7584 panic!("expected workers command");
7585 };
7586
7587 match workers_cmd.command {
7588 super::WorkersSubCommand::List(list_cmd) => {
7589 assert!(list_cmd.all);
7590 assert!(!list_cmd.json);
7591 }
7592 _ => panic!("expected workers list command"),
7593 }
7594 }
7595
7596 #[test]
7597 fn parses_workers_logs_follow_and_build_flags() {
7598 let cli = Cli::parse_from([
7599 "xbp",
7600 "workers",
7601 "logs",
7602 "-f",
7603 "--build",
7604 "--failed",
7605 "xbp-production",
7606 ]);
7607
7608 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7609 panic!("expected workers command");
7610 };
7611
7612 match workers_cmd.command {
7613 super::WorkersSubCommand::Logs(logs_cmd) => {
7614 assert!(logs_cmd.follow);
7615 assert!(logs_cmd.build);
7616 assert!(logs_cmd.failed);
7617 assert_eq!(logs_cmd.script_name.as_deref(), Some("xbp-production"));
7618 }
7619 _ => panic!("expected workers logs command"),
7620 }
7621 }
7622
7623 #[test]
7624 fn parses_workers_logs_worker_and_wait_flags() {
7625 let cli = Cli::parse_from([
7626 "xbp",
7627 "workers",
7628 "logs",
7629 "--worker",
7630 "suits-formations",
7631 "--json",
7632 "--wait",
7633 "--wait-seconds",
7634 "60",
7635 ]);
7636
7637 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7638 panic!("expected workers command");
7639 };
7640
7641 match workers_cmd.command {
7642 super::WorkersSubCommand::Logs(logs_cmd) => {
7643 assert_eq!(logs_cmd.target.worker.as_deref(), Some("suits-formations"));
7644 assert!(logs_cmd.json);
7645 assert!(logs_cmd.wait);
7646 assert_eq!(logs_cmd.wait_seconds, 60);
7647 }
7648 _ => panic!("expected workers logs command"),
7649 }
7650 }
7651
7652 #[test]
7653 fn parses_workers_logs_output_and_filter_flags() {
7654 let cli = Cli::parse_from([
7655 "xbp",
7656 "workers",
7657 "logs",
7658 "--build",
7659 "-n",
7660 "200",
7661 "-o",
7662 "build.log",
7663 "-g",
7664 "error",
7665 "--errors-only",
7666 "--list-builds",
7667 "--build-index",
7668 "1",
7669 ]);
7670
7671 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7672 panic!("expected workers command");
7673 };
7674
7675 match workers_cmd.command {
7676 super::WorkersSubCommand::Logs(logs_cmd) => {
7677 assert!(logs_cmd.build);
7678 assert_eq!(logs_cmd.lines, Some(200));
7679 assert_eq!(
7680 logs_cmd.output.as_deref().and_then(|path| path.to_str()),
7681 Some("build.log")
7682 );
7683 assert_eq!(logs_cmd.grep.as_deref(), Some("error"));
7684 assert!(logs_cmd.errors_only);
7685 assert!(logs_cmd.list_builds);
7686 assert_eq!(logs_cmd.build_index, Some(1));
7687 }
7688 _ => panic!("expected workers logs command"),
7689 }
7690 }
7691
7692 #[test]
7693 fn parses_workers_list_filter_and_limit_flags() {
7694 let cli = Cli::parse_from([
7695 "xbp", "workers", "list", "--failed", "-n", "5", "--sort", "modified",
7696 ]);
7697
7698 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7699 panic!("expected workers command");
7700 };
7701
7702 match workers_cmd.command {
7703 super::WorkersSubCommand::List(list_cmd) => {
7704 assert!(list_cmd.failed);
7705 assert_eq!(list_cmd.limit, Some(5));
7706 assert_eq!(list_cmd.sort, "modified");
7707 }
7708 _ => panic!("expected workers list command"),
7709 }
7710 }
7711
7712 #[test]
7713 fn parses_worker_alias_command() {
7714 let cli = Cli::parse_from(["xbp", "worker", "env", "--show-values"]);
7715
7716 let Some(Commands::Workers(workers_cmd)) = cli.command else {
7717 panic!("expected workers command through alias");
7718 };
7719
7720 match workers_cmd.command {
7721 super::WorkersSubCommand::Env(env_cmd) => {
7722 assert!(env_cmd.show_values);
7723 }
7724 _ => panic!("expected workers env command"),
7725 }
7726 }
7727
7728 #[test]
7729 fn parses_dns_providers_command() {
7730 let cli = Cli::parse_from(["xbp", "dns", "providers"]);
7731
7732 match cli.command {
7733 Some(Commands::Dns(dns_cmd)) => {
7734 assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
7735 }
7736 _ => panic!("expected dns command"),
7737 }
7738 }
7739
7740 #[test]
7741 fn dns_zone_list_defaults_provider_to_cloudflare() {
7742 let cli = Cli::parse_from(["xbp", "dns", "zones", "list"]);
7743
7744 let Some(Commands::Dns(dns_cmd)) = cli.command else {
7745 panic!("expected dns command");
7746 };
7747
7748 match dns_cmd.command {
7749 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
7750 DnsZonesSubCommand::List(list_cmd) => {
7751 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
7752 }
7753 _ => panic!("expected zones list command"),
7754 },
7755 _ => panic!("expected zones command"),
7756 }
7757 }
7758
7759 #[test]
7760 fn dns_record_list_defaults_provider_to_cloudflare() {
7761 let cli = Cli::parse_from(["xbp", "dns", "records", "list", "--zone-id", "zone_123"]);
7762
7763 let Some(Commands::Dns(dns_cmd)) = cli.command else {
7764 panic!("expected dns command");
7765 };
7766
7767 match dns_cmd.command {
7768 DnsSubCommand::Records(records_cmd) => match records_cmd.command {
7769 super::DnsRecordsSubCommand::List(list_cmd) => {
7770 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
7771 assert_eq!(list_cmd.zone_id, "zone_123");
7772 }
7773 _ => panic!("expected records list command"),
7774 },
7775 _ => panic!("expected records command"),
7776 }
7777 }
7778
7779 #[test]
7780 fn dns_help_includes_descriptions_and_examples() {
7781 let err = Cli::try_parse_from(["xbp", "dns", "-h"]).expect_err("help");
7782 let rendered = err.to_string();
7783
7784 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
7785 assert!(rendered.contains("Manage DNS providers, zones, records, DNSSEC, and settings"));
7786 assert!(rendered.contains("List supported DNS providers and current implementation status"));
7787 assert!(rendered.contains("xbp dns records create"));
7788 }
7789
7790 #[test]
7791 fn dns_providers_help_includes_discovery_note() {
7792 let err = Cli::try_parse_from(["xbp", "dns", "providers", "-h"]).expect_err("help");
7793 let rendered = err.to_string();
7794
7795 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
7796 assert!(rendered.contains("Implemented providers are wired into `xbp dns` today."));
7797 }
7798
7799 #[test]
7800 fn dns_records_without_subcommand_displays_help_screen() {
7801 let err = Cli::try_parse_from(["xbp", "dns", "records"]).expect_err("missing subcommand");
7802 let rendered = err.to_string();
7803
7804 assert!(matches!(
7805 err.kind(),
7806 clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
7807 | clap::error::ErrorKind::MissingSubcommand
7808 ));
7809 assert!(rendered.contains("List, create, edit, import, export, and batch DNS records"));
7810 assert!(rendered.contains("Create a new DNS record"));
7811 assert!(rendered.contains("xbp dns records import"));
7812 }
7813
7814 #[test]
7815 fn parses_dns_zone_list_command() {
7816 let cli = Cli::parse_from([
7817 "xbp",
7818 "dns",
7819 "zones",
7820 "list",
7821 "--provider",
7822 "cloudflare",
7823 "--account-name-op",
7824 "contains",
7825 "--type",
7826 "full,partial",
7827 ]);
7828
7829 match cli.command {
7830 Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
7831 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
7832 DnsZonesSubCommand::List(list_cmd) => {
7833 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
7834 assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
7835 assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
7836 }
7837 _ => panic!("expected dns zones list"),
7838 },
7839 _ => panic!("expected dns zones"),
7840 },
7841 _ => panic!("expected dns command"),
7842 }
7843 }
7844
7845 #[test]
7846 fn parses_domains_search_command() {
7847 let cli = Cli::parse_from([
7848 "xbp",
7849 "domains",
7850 "search",
7851 "--query",
7852 "xbp",
7853 "--extension",
7854 "com",
7855 ]);
7856
7857 match cli.command {
7858 Some(Commands::Domains(domains_cmd)) => {
7859 assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
7860 assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
7861 }
7862 _ => panic!("expected domains command"),
7863 }
7864 }
7865
7866 #[test]
7867 fn parses_cloudflare_config_account_id_command() {
7868 let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
7869
7870 match cli.command {
7871 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
7872 Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
7873 assert!(matches!(
7874 cloudflare_cmd.action,
7875 Some(CloudflareConfigAction::SetAccountId { .. })
7876 ));
7877 }
7878 _ => panic!("expected cloudflare config provider"),
7879 },
7880 _ => panic!("expected config command"),
7881 }
7882 }
7883
7884 #[test]
7885 fn parses_runners_groups_update_command() {
7886 let cli = Cli::parse_from([
7887 "xbp",
7888 "runners",
7889 "groups",
7890 "update",
7891 "--organization-id",
7892 "org_123",
7893 "--name",
7894 "linux-builders",
7895 "--visibility",
7896 "selected",
7897 "--restricted-to-workflows",
7898 ]);
7899
7900 let Some(Commands::Runners(runners_cmd)) = cli.command else {
7901 panic!("expected runners command");
7902 };
7903
7904 match runners_cmd.command {
7905 super::RunnersSubCommand::Groups(groups_cmd) => match groups_cmd.command {
7906 super::RunnersGroupsSubCommand::Update(update_cmd) => {
7907 assert_eq!(update_cmd.organization_id, "org_123");
7908 assert_eq!(update_cmd.name, "linux-builders");
7909 assert_eq!(update_cmd.visibility.as_deref(), Some("selected"));
7910 assert!(update_cmd.restricted_to_workflows);
7911 }
7912 _ => panic!("expected runners groups update command"),
7913 },
7914 _ => panic!("expected runners groups command"),
7915 }
7916 }
7917
7918 #[test]
7919 fn parses_api_runners_group_repositories_create_command() {
7920 let cli = Cli::parse_from([
7921 "xbp",
7922 "api",
7923 "runners",
7924 "group-repositories-create",
7925 "group_123",
7926 "--repository-owner",
7927 "xylex-group",
7928 "--repository-name",
7929 "xbp",
7930 "--repository-full-name",
7931 "xylex-group/xbp",
7932 "--is-private",
7933 ]);
7934
7935 let Some(Commands::Api(api_cmd)) = cli.command else {
7936 panic!("expected api command");
7937 };
7938
7939 match api_cmd.command {
7940 super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
7941 super::ApiRunnersSubCommand::GroupRepositoriesCreate(create_cmd) => {
7942 assert_eq!(create_cmd.runner_group_id, "group_123");
7943 assert_eq!(create_cmd.repository_owner, "xylex-group");
7944 assert_eq!(create_cmd.repository_name, "xbp");
7945 assert_eq!(create_cmd.repository_full_name, "xylex-group/xbp");
7946 assert!(create_cmd.is_private);
7947 }
7948 _ => panic!("expected api runners group repositories create command"),
7949 },
7950 _ => panic!("expected api runners command"),
7951 }
7952 }
7953
7954 #[test]
7955 fn parses_runners_host_preflight_command() {
7956 let cli = Cli::parse_from([
7957 "xbp",
7958 "runners",
7959 "hosts",
7960 "preflight",
7961 "--runner-host-id",
7962 "host_123",
7963 "--write-api",
7964 ]);
7965
7966 let Some(Commands::Runners(runners_cmd)) = cli.command else {
7967 panic!("expected runners command");
7968 };
7969
7970 match runners_cmd.command {
7971 super::RunnersSubCommand::Hosts(hosts_cmd) => match hosts_cmd.command {
7972 super::RunnersHostsSubCommand::Preflight(preflight_cmd) => {
7973 assert_eq!(preflight_cmd.runner_host_id, "host_123");
7974 assert!(preflight_cmd.write_api);
7975 }
7976 _ => panic!("expected runners hosts preflight command"),
7977 },
7978 _ => panic!("expected runners hosts command"),
7979 }
7980 }
7981
7982 #[test]
7983 fn parses_runners_agent_serve_command() {
7984 let cli = Cli::parse_from([
7985 "xbp",
7986 "runners",
7987 "agent",
7988 "serve",
7989 "--runner-host-id",
7990 "host_123",
7991 "--interval-seconds",
7992 "5",
7993 "--once",
7994 ]);
7995
7996 let Some(Commands::Runners(runners_cmd)) = cli.command else {
7997 panic!("expected runners command");
7998 };
7999
8000 match runners_cmd.command {
8001 super::RunnersSubCommand::Agent(agent_cmd) => match agent_cmd.command {
8002 super::RunnersAgentSubCommand::Serve(serve_cmd) => {
8003 assert_eq!(serve_cmd.runner_host_id, "host_123");
8004 assert_eq!(serve_cmd.interval_seconds, 5);
8005 assert!(serve_cmd.once);
8006 }
8007 },
8008 _ => panic!("expected runners agent command"),
8009 }
8010 }
8011
8012 #[test]
8013 fn parses_api_runners_host_preflight_upsert_command() {
8014 let cli = Cli::parse_from([
8015 "xbp",
8016 "api",
8017 "runners",
8018 "hosts-preflights-upsert",
8019 "--runner-host-id",
8020 "host_123",
8021 "--platform",
8022 "linux",
8023 "--status",
8024 "synced",
8025 "--service-manager",
8026 "systemd",
8027 ]);
8028
8029 let Some(Commands::Api(api_cmd)) = cli.command else {
8030 panic!("expected api command");
8031 };
8032
8033 match api_cmd.command {
8034 super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
8035 super::ApiRunnersSubCommand::HostsPreflightsUpsert(upsert_cmd) => {
8036 assert_eq!(upsert_cmd.runner_host_id, "host_123");
8037 assert_eq!(upsert_cmd.platform, "linux");
8038 assert_eq!(upsert_cmd.status, "synced");
8039 assert_eq!(upsert_cmd.service_manager.as_deref(), Some("systemd"));
8040 }
8041 _ => panic!("expected api runners host preflight upsert command"),
8042 },
8043 _ => panic!("expected api runners command"),
8044 }
8045 }
8046
8047 #[cfg(feature = "linear")]
8048 #[test]
8049 fn parses_linear_list_and_todos_sync() {
8050 let cli = Cli::parse_from([
8051 "xbp",
8052 "linear",
8053 "list",
8054 "--team",
8055 "XLX",
8056 "--assignee",
8057 "me",
8058 "--limit",
8059 "10",
8060 ]);
8061 let Some(Commands::Linear(cmd)) = cli.command else {
8062 panic!("expected linear command");
8063 };
8064 match cmd.command {
8065 Some(super::LinearSubCommand::List(list)) => {
8066 assert_eq!(list.team.as_deref(), Some("XLX"));
8067 assert_eq!(list.assignee.as_deref(), Some("me"));
8068 assert_eq!(list.limit, 10);
8069 }
8070 _ => panic!("expected linear list"),
8071 }
8072
8073 let cli = Cli::parse_from([
8074 "xbp",
8075 "todos",
8076 "sync",
8077 "--to",
8078 "linear",
8079 "--dry-run",
8080 "--yes",
8081 "--team",
8082 "XLX",
8083 ]);
8084 let Some(Commands::Todos(cmd)) = cli.command else {
8085 panic!("expected todos command");
8086 };
8087 match cmd.command {
8088 Some(super::TodosSubCommand::Sync(sync)) => {
8089 assert_eq!(sync.to, Some(super::TodosSyncTarget::Linear));
8090 assert!(sync.dry_run);
8091 assert!(sync.yes);
8092 assert_eq!(sync.team.as_deref(), Some("XLX"));
8093 }
8094 _ => panic!("expected todos sync"),
8095 }
8096
8097 let cli = Cli::parse_from([
8098 "xbp",
8099 "issues",
8100 "sync",
8101 "--to",
8102 "linear",
8103 "--dry-run",
8104 "--yes",
8105 "--team",
8106 "XLX",
8107 ]);
8108 let Some(Commands::Issues(cmd)) = cli.command else {
8109 panic!("expected issues command");
8110 };
8111 match cmd.command {
8112 Some(super::TodosSubCommand::Sync(sync)) => {
8113 assert_eq!(sync.to, Some(super::TodosSyncTarget::Linear));
8114 assert!(sync.dry_run);
8115 assert!(sync.yes);
8116 assert_eq!(sync.team.as_deref(), Some("XLX"));
8117 }
8118 _ => panic!("expected issues sync"),
8119 }
8120
8121 let cli = Cli::parse_from(["xbp", "gh", "show", "42"]);
8122 let Some(Commands::Github(cmd)) = cli.command else {
8123 panic!("expected github command");
8124 };
8125 match cmd.command {
8126 Some(super::GithubSubCommand::Show(show)) => {
8127 assert_eq!(show.id, "42");
8128 }
8129 _ => panic!("expected github show"),
8130 }
8131 }
8132
8133 #[cfg(feature = "linear")]
8134 #[test]
8135 fn parses_linear_search_with_tui() {
8136 let cli = Cli::parse_from([
8137 "xbp", "linear", "search", "cache", "--team", "XLX", "--state", "done", "--sort",
8138 "priority", "--tui",
8139 ]);
8140 let Some(Commands::Linear(cmd)) = cli.command else {
8141 panic!("expected linear command");
8142 };
8143 match cmd.command {
8144 Some(super::LinearSubCommand::Search(search)) => {
8145 assert_eq!(search.query, "cache");
8146 assert_eq!(search.team.as_deref(), Some("XLX"));
8147 assert_eq!(search.state.as_deref(), Some("done"));
8148 assert_eq!(search.sort, "priority");
8149 assert!(search.tui);
8150 }
8151 _ => panic!("expected linear search"),
8152 }
8153 }
8154
8155 #[test]
8156 fn parses_singular_issue_search() {
8157 let cli = Cli::parse_from([
8158 "xbp",
8159 "issue",
8160 "search",
8161 "cache",
8162 "--github",
8163 "--owner",
8164 "xylex-group",
8165 "--repo",
8166 "xbp",
8167 "--limit",
8168 "25",
8169 ]);
8170 let Some(Commands::Issues(cmd)) = cli.command else {
8171 panic!("expected issue command");
8172 };
8173 match cmd.command {
8174 Some(super::TodosSubCommand::Search(search)) => {
8175 assert_eq!(search.query, "cache");
8176 assert!(search.github);
8177 assert_eq!(search.owner.as_deref(), Some("xylex-group"));
8178 assert_eq!(search.repo.as_deref(), Some("xbp"));
8179 assert_eq!(search.limit, 25);
8180 }
8181 _ => panic!("expected issue search"),
8182 }
8183 }
8184
8185 #[cfg(not(feature = "linear"))]
8186 #[test]
8187 fn parses_github_show_without_linear_feature() {
8188 let cli = Cli::parse_from(["xbp", "gh", "show", "42"]);
8189 let Some(Commands::Github(cmd)) = cli.command else {
8190 panic!("expected github command");
8191 };
8192 match cmd.command {
8193 Some(super::GithubSubCommand::Show(show)) => {
8194 assert_eq!(show.id, "42");
8195 }
8196 _ => panic!("expected github show"),
8197 }
8198 }
8199}