1use clap::{Args, Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
5#[command(
6 name = "xbp",
7 version,
8 about = "Deploy, operate, and debug services with one CLI.",
9 long_about = "XBP is an operations-first CLI for deployments, diagnostics, service orchestration,\nnetwork controls, and runtime observability.",
10 disable_help_subcommand = false,
11 next_line_help = true,
12 help_template = "{before-help}{name} {version}\n{about-with-newline}\
13{usage-heading} {usage}\n\n\
14{all-args}\
15{after-help}",
16 after_help = "Quick start:\n xbp diag\n xbp services\n xbp service start <name>\n xbp api install --port 8080\n\nUse `xbp <command> -h` for command-specific examples."
17)]
18pub struct Cli {
19 #[arg(long, global = true, help = "Enable verbose debugging output")]
20 pub debug: bool,
21 #[arg(short = 'l', help = "List pm2 processes")]
22 pub list: bool,
23 #[arg(short = 'p', long = "port", help = "Filter by port number")]
24 pub port: Option<u16>,
25 #[arg(long, help = "Open logs directory")]
26 pub logs: bool,
27
28 #[command(subcommand)]
29 pub command: Option<Commands>,
30}
31
32#[derive(Subcommand, Debug)]
33pub enum Commands {
34 #[command(about = "Inspect or manage listening ports")]
35 Ports(PortsCmd),
36 #[command(
37 about = "Analyze the current git worktree and create a conventional commit",
38 visible_alias = "c"
39 )]
40 Commit(CommitCmd),
41 #[command(about = "Initialize an XBP project in the current directory")]
42 Init,
43 #[command(about = "Install common dependencies for host setup")]
44 Setup,
45 #[command(about = "Redeploy one service or the entire project")]
46 Redeploy {
47 #[arg(
48 help = "Service name to redeploy (optional, uses legacy redeploy.sh if not provided)"
49 )]
50 service_name: Option<String>,
51 },
52 #[command(about = "Run the legacy remote redeploy workflow over SSH")]
53 RedeployV2(RedeployV2Cmd),
54 #[command(about = "Inspect project/global config and manage provider keys")]
55 Config(ConfigCmd),
56 #[command(
57 about = "Install supported host packages or project tooling",
58 after_help = crate::commands::INSTALL_COMMAND_AFTER_HELP
59 )]
60 Install {
61 #[arg(short = 'l', long = "list", help = "List installable targets and exit")]
62 list: bool,
63 #[arg(help = "Install target (leave empty to show installable options)")]
64 package: Option<String>,
65 },
66 #[command(about = "Tail local or remote logs")]
67 Logs(LogsCmd),
68 #[command(
69 about = "Open an interactive remote shell over SSH",
70 visible_alias = "shell"
71 )]
72 Ssh(SshCmd),
73 #[command(about = "Open or manage cloudflared TCP forwarders")]
74 Cloudflared(CloudflaredCmd),
75 #[command(about = "List PM2 processes")]
76 List,
77 #[command(about = "Fetch an HTTP endpoint with sane defaults")]
78 Curl(CurlCmd),
79 #[command(about = "List configured services from project config")]
80 Services,
81 #[command(about = "Run service-level commands (build/install/start/dev)")]
82 Service {
83 #[arg(help = "Command to run: build, install, start, dev, or --help")]
84 command: Option<String>,
85 #[arg(help = "Service name")]
86 service_name: Option<String>,
87 },
88 #[command(about = "Manage NGINX site configs and upstream mappings")]
89 Nginx(NginxCmd),
90 #[command(about = "Manage host network configuration and floating IPs")]
91 Network(NetworkCmd),
92 #[command(about = "Run full system diagnostics and readiness checks")]
93 Diag(DiagCmd),
94 #[command(about = "Run health-check monitoring commands")]
95 Monitor(MonitorCmd),
96 #[command(about = "Capture a PM2 snapshot for later restore")]
97 Snapshot,
98 #[command(about = "Restore PM2 state from dump or latest snapshot")]
99 Resurrect,
100 #[command(about = "Stop a PM2 process by name or stop all")]
101 Stop {
102 #[arg(help = "PM2 process name or 'all' (default: all)")]
103 target: Option<String>,
104 },
105 #[command(about = "Flush PM2 logs globally or for a specific process")]
106 Flush {
107 #[arg(help = "Optional PM2 process name")]
108 target: Option<String>,
109 },
110 #[command(about = "Run login flow against configured XBP API")]
111 Login,
112 #[command(
113 about = "Inspect, reconcile, or bump project versions",
114 visible_alias = "v"
115 )]
116 Version(VersionCmd),
117 #[command(about = "Run configured npm/crates publish workflows for the current XBP project")]
118 Publish(PublishCmd),
119 #[command(about = "Show PM2 environment by name or numeric id")]
120 Env {
121 #[arg(help = "PM2 process name or id")]
122 target: String,
123 },
124 #[command(about = "Tail app logs or Kafka logs")]
125 Tail(TailCmd),
126 #[command(about = "Start a binary/process under PM2")]
127 Start {
128 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
129 args: Vec<String>,
130 },
131 #[command(about = "Generate helper artifacts such as systemd units")]
132 Generate(GenerateCmd),
133 #[cfg(feature = "secrets")]
134 #[command(about = "Manage env vars and GitHub Actions environment variables (feature-gated)")]
135 Secrets(SecretsCmd),
136 #[command(about = "Manage DNS providers and records")]
137 Dns(DnsCmd),
138 #[command(about = "Discover and inspect registered domains")]
139 Domains(DomainsCmd),
140 #[command(
141 about = "Generate 'what did I get done' Markdown report from git commits across repos"
142 )]
143 Done(DoneCmd),
144 #[cfg(feature = "kubernetes")]
145 #[command(about = "Experimental Kubernetes cluster manager (feature-gated)")]
146 Kubernetes(KubernetesCmd),
147 #[cfg(feature = "nordvpn")]
148 #[command(about = "NordVPN meshnet setup and passthrough (feature-gated)")]
149 Nordvpn(NordvpnCmd),
150 #[cfg(feature = "monitoring")]
151 Monitoring(MonitoringCmd),
152 #[command(about = "Manage the XBP API server")]
153 Api(ApiCmd),
154 #[cfg(feature = "docker")]
155 #[command(about = "Pass-through wrapper around the Docker CLI")]
156 Docker(DockerCmd),
157}
158
159pub fn command_label(command: &Commands) -> &'static str {
160 match command {
161 Commands::Ports(_) => "ports",
162 Commands::Commit(_) => "commit",
163 Commands::Init => "init",
164 Commands::Setup => "setup",
165 Commands::Redeploy { .. } => "redeploy",
166 Commands::RedeployV2(_) => "redeploy-v2",
167 Commands::Config(_) => "config",
168 Commands::Install { .. } => "install",
169 Commands::Logs(_) => "logs",
170 Commands::Ssh(_) => "ssh",
171 Commands::Cloudflared(_) => "cloudflared",
172 Commands::List => "list",
173 Commands::Curl(_) => "curl",
174 Commands::Services => "services",
175 Commands::Service { .. } => "service",
176 Commands::Nginx(_) => "nginx",
177 Commands::Network(_) => "network",
178 Commands::Diag(_) => "diag",
179 Commands::Monitor(_) => "monitor",
180 Commands::Snapshot => "snapshot",
181 Commands::Resurrect => "resurrect",
182 Commands::Stop { .. } => "stop",
183 Commands::Flush { .. } => "flush",
184 Commands::Login => "login",
185 Commands::Version(_) => "version",
186 Commands::Publish(_) => "publish",
187 Commands::Env { .. } => "env",
188 Commands::Tail(_) => "tail",
189 Commands::Start { .. } => "start",
190 Commands::Generate(_) => "generate",
191 #[cfg(feature = "secrets")]
192 Commands::Secrets(_) => "secrets",
193 Commands::Dns(_) => "dns",
194 Commands::Domains(_) => "domains",
195 Commands::Done(_) => "done",
196 #[cfg(feature = "kubernetes")]
197 Commands::Kubernetes(_) => "kubernetes",
198 #[cfg(feature = "nordvpn")]
199 Commands::Nordvpn(_) => "nordvpn",
200 #[cfg(feature = "monitoring")]
201 Commands::Monitoring(_) => "monitoring",
202 Commands::Api(_) => "api",
203 #[cfg(feature = "docker")]
204 Commands::Docker(_) => "docker",
205 }
206}
207
208#[derive(Args, Debug)]
209pub struct CommitCmd {
210 #[arg(
211 long,
212 help = "Generate and print the conventional commit message without creating a git commit"
213 )]
214 pub dry_run: bool,
215 #[arg(
216 short = 'p',
217 long,
218 help = "Push after committing, or push pending local commits when nothing new needs committing"
219 )]
220 pub push: bool,
221 #[arg(long, help = "Skip OpenRouter and use local heuristics only")]
222 pub no_ai: bool,
223 #[arg(
224 long,
225 default_value = "openai/gpt-4o-mini",
226 help = "OpenRouter model override used for commit generation"
227 )]
228 pub model: String,
229 #[arg(
230 long,
231 help = "Force the conventional commit scope (for example: cli, api, docs)"
232 )]
233 pub scope: Option<String>,
234}
235
236#[derive(Args, Debug)]
237pub struct PortsCmd {
238 #[arg(short = 'p', long = "port")]
239 pub port: Option<u16>,
240 #[arg(long = "kill")]
241 pub kill: bool,
242 #[arg(short = 'n', long = "nginx")]
243 pub nginx: bool,
244 #[arg(
245 long = "full",
246 help = "Show one unified ports view (reconciled listeners + exposure + security flags)"
247 )]
248 pub full: bool,
249 #[arg(
250 long = "no-local",
251 help = "Exclude connections where LocalAddr equals RemoteAddr"
252 )]
253 pub no_local: bool,
254 #[arg(
255 long = "exposure",
256 help = "Diagnose external exposure per port (binding + firewall layer)"
257 )]
258 pub exposure: bool,
259}
260
261#[derive(Args, Debug)]
262pub struct ConfigCmd {
263 #[arg(
264 long,
265 help = "Show the current project config instead of opening global XBP paths"
266 )]
267 pub project: bool,
268 #[arg(long, help = "Print global XBP paths without opening them")]
269 pub no_open: bool,
270 #[command(subcommand)]
271 pub provider: Option<ConfigProviderCmd>,
272}
273
274#[derive(Subcommand, Debug)]
275pub enum ConfigProviderCmd {
276 #[command(about = "Manage the OpenRouter API key used by AI-enabled commands")]
277 Openrouter(ConfigSecretCmd),
278 #[command(about = "Manage the GitHub OAuth2 token used for release automation")]
279 Github(ConfigSecretCmd),
280 #[command(about = "Manage Cloudflare API credentials used by secrets, DNS, and domains")]
281 Cloudflare(CloudflareConfigCmd),
282 #[command(
283 about = "Manage the Linear API key used for release-note issue linking and initiative publishing"
284 )]
285 Linear(LinearConfigCmd),
286 #[command(about = "Manage npm registry auth and guided npm publish config")]
287 Npm(RegistryConfigCmd),
288 #[command(about = "Manage crates.io auth and guided crate publish config")]
289 Crates(RegistryConfigCmd),
290}
291
292#[derive(Args, Debug)]
293pub struct ConfigSecretCmd {
294 #[command(subcommand)]
295 pub action: ConfigSecretAction,
296}
297
298#[derive(Subcommand, Debug)]
299pub enum ConfigSecretAction {
300 #[command(about = "Set provider key (omit value to enter it securely)")]
301 SetKey {
302 #[arg(help = "Provider key/token value")]
303 key: Option<String>,
304 },
305 #[command(about = "Delete the stored provider key")]
306 DeleteKey,
307 #[command(about = "Show whether a key is configured (masked by default)")]
308 Show {
309 #[arg(long, help = "Print full key/token value (not masked)")]
310 raw: bool,
311 },
312}
313
314#[derive(Args, Debug)]
315pub struct CloudflareConfigCmd {
316 #[command(subcommand)]
317 pub action: CloudflareConfigAction,
318}
319
320#[derive(Subcommand, Debug)]
321pub enum CloudflareConfigAction {
322 #[command(about = "Set Cloudflare API token (omit value to enter it securely)")]
323 SetKey {
324 #[arg(help = "Cloudflare API token")]
325 key: Option<String>,
326 },
327 #[command(about = "Delete the stored Cloudflare API token")]
328 DeleteKey,
329 #[command(about = "Show whether a Cloudflare API token is configured")]
330 ShowKey {
331 #[arg(long, help = "Print full token value (not masked)")]
332 raw: bool,
333 },
334 #[command(about = "Set the default Cloudflare account ID")]
335 SetAccountId {
336 #[arg(help = "Cloudflare account ID")]
337 account_id: Option<String>,
338 },
339 #[command(about = "Delete the stored default Cloudflare account ID")]
340 DeleteAccountId,
341 #[command(about = "Show whether a Cloudflare account ID is configured")]
342 ShowAccountId {
343 #[arg(long, help = "Print full account ID value (not masked)")]
344 raw: bool,
345 },
346}
347
348#[derive(Args, Debug)]
349pub struct LinearConfigCmd {
350 #[command(subcommand)]
351 pub action: LinearConfigAction,
352}
353
354#[derive(Subcommand, Debug)]
355pub enum LinearConfigAction {
356 #[command(about = "Set Linear API key (omit value to enter it securely)")]
357 SetKey {
358 #[arg(help = "Linear API key/token value")]
359 key: Option<String>,
360 },
361 #[command(about = "Delete the stored Linear API key")]
362 DeleteKey,
363 #[command(about = "Show whether a Linear API key is configured (masked by default)")]
364 Show {
365 #[arg(long, help = "Print full key/token value (not masked)")]
366 raw: bool,
367 },
368 #[command(
369 name = "select-initiative",
370 about = "Pick a Linear initiative for the current repo and save it to .xbp/xbp.yaml"
371 )]
372 SelectInitiative,
373}
374
375#[derive(Args, Debug)]
376pub struct RegistryConfigCmd {
377 #[command(subcommand)]
378 pub action: RegistryConfigAction,
379}
380
381#[derive(Subcommand, Debug)]
382pub enum RegistryConfigAction {
383 #[command(about = "Set registry token/key (omit value to enter it securely)")]
384 SetKey {
385 #[arg(help = "Registry token value")]
386 key: Option<String>,
387 },
388 #[command(about = "Delete the stored registry token")]
389 DeleteKey,
390 #[command(about = "Show whether a registry token is configured (masked by default)")]
391 Show {
392 #[arg(long, help = "Print full token value (not masked)")]
393 raw: bool,
394 },
395 #[command(
396 name = "setup-release",
397 about = "Interactively configure project publish settings in .xbp/xbp.yaml"
398 )]
399 SetupRelease,
400}
401
402#[derive(Args, Debug)]
403pub struct CurlCmd {
404 #[arg(help = "URL or domain to fetch, e.g. example.com or https://example.com/api")]
405 pub url: Option<String>,
406 #[arg(long, help = "Disable the default 15 second timeout")]
407 pub no_timeout: bool,
408}
409
410#[derive(Args, Debug)]
411#[command(subcommand_precedence_over_arg = true)]
412pub struct VersionCmd {
413 #[arg(
414 help = "Show versions, bump with major/minor/patch, or set an explicit version like 1.2.3"
415 )]
416 pub target: Option<String>,
417 #[arg(
418 short = 'v',
419 long = "version",
420 help = "Explicit version target; equivalent to the positional version value and overrides it when both are provided"
421 )]
422 pub explicit_version: Option<String>,
423 #[arg(long, help = "Show normalized git tags from `git tag --list`")]
424 pub git: bool,
425 #[command(subcommand)]
426 pub command: Option<VersionSubCommand>,
427}
428
429#[derive(Subcommand, Debug)]
430pub enum VersionSubCommand {
431 #[command(
432 about = "Create and push a git tag for this version, then create a GitHub release",
433 visible_alias = "r"
434 )]
435 Release(VersionReleaseCmd),
436 #[command(
437 about = "Manage Rust workspace release/version drift, sync, validation, and publish flow",
438 arg_required_else_help = true
439 )]
440 Workspace(VersionWorkspaceCmd),
441}
442
443#[derive(Args, Debug)]
444pub struct VersionReleaseCmd {
445 #[arg(
446 long,
447 help = "Release this version instead of auto-detecting from tracked files"
448 )]
449 pub version: Option<String>,
450 #[arg(
451 long,
452 help = "Allow releasing with uncommitted changes in the working tree"
453 )]
454 pub allow_dirty: bool,
455 #[arg(long, help = "Release title (defaults to <version> - <repo>)")]
456 pub title: Option<String>,
457 #[arg(long, help = "Release notes body (Markdown)")]
458 pub notes: Option<String>,
459 #[arg(long, help = "Read release notes body from a file")]
460 pub notes_file: Option<PathBuf>,
461 #[arg(long, help = "Create as draft release")]
462 pub draft: bool,
463 #[arg(long, help = "Mark release as pre-release")]
464 pub prerelease: bool,
465 #[arg(
466 long,
467 help = "Run configured npm/crates publish workflows before creating the GitHub release"
468 )]
469 pub publish: bool,
470 #[arg(
471 long,
472 value_enum,
473 default_value_t = VersionReleaseLatest::Legacy,
474 help = "Control GitHub latest flag: true, false, or legacy"
475 )]
476 pub make_latest: VersionReleaseLatest,
477}
478
479#[derive(Copy, Clone, Debug, ValueEnum)]
480pub enum VersionReleaseLatest {
481 True,
482 False,
483 Legacy,
484}
485
486#[derive(Args, Debug)]
487pub struct PublishCmd {
488 #[arg(
489 long,
490 help = "Validate and print what would publish without uploading packages"
491 )]
492 pub dry_run: bool,
493 #[arg(
494 long,
495 help = "Allow publish workflows to run with a dirty working tree"
496 )]
497 pub allow_dirty: bool,
498 #[arg(long, help = "Limit publishing to one target: npm or crates")]
499 pub target: Option<String>,
500}
501
502#[derive(Args, Debug)]
503#[command(
504 after_help = "Examples:\n xbp version workspace check --repo C:/Users/floris/Documents/GitHub/athena\n xbp version workspace sync --version 3.16.5\n xbp version workspace sync --version 3.16.5 --write\n xbp version workspace validate --cargo-check --package-dry-run\n xbp version workspace publish plan\n xbp version workspace publish run --dry-run\n xbp version workspace publish run --from athena-s3"
505)]
506pub struct VersionWorkspaceCmd {
507 #[command(subcommand)]
508 pub command: VersionWorkspaceSubCommand,
509}
510
511#[derive(Args, Debug, Clone, Default)]
512pub struct VersionWorkspaceTargetArgs {
513 #[arg(
514 long,
515 help = "Workspace repo root to inspect (defaults to current project root)"
516 )]
517 pub repo: Option<PathBuf>,
518 #[arg(long, help = "Emit machine-readable JSON output")]
519 pub json: bool,
520}
521
522#[derive(Subcommand, Debug)]
523pub enum VersionWorkspaceSubCommand {
524 #[command(about = "Detect workspace release drift and exit non-zero when mismatches exist")]
525 Check(VersionWorkspaceCheckCmd),
526 #[command(about = "Preview or apply workspace-wide version alignment")]
527 Sync(VersionWorkspaceSyncCmd),
528 #[command(about = "Run structural and optional cargo validation for workspace publishability")]
529 Validate(VersionWorkspaceValidateCmd),
530 #[command(about = "Plan or execute crates.io publishing for workspace packages")]
531 Publish(VersionWorkspacePublishCmd),
532}
533
534#[derive(Args, Debug)]
535pub struct VersionWorkspaceCheckCmd {
536 #[command(flatten)]
537 pub target: VersionWorkspaceTargetArgs,
538 #[arg(
539 long,
540 help = "Expected release version (defaults to the root package version)"
541 )]
542 pub version: Option<String>,
543}
544
545#[derive(Args, Debug)]
546pub struct VersionWorkspaceSyncCmd {
547 #[command(flatten)]
548 pub target: VersionWorkspaceTargetArgs,
549 #[arg(
550 long,
551 help = "Target release version (defaults to the root package version)"
552 )]
553 pub version: Option<String>,
554 #[arg(
555 long,
556 help = "Write changes to disk instead of previewing the sync plan"
557 )]
558 pub write: bool,
559}
560
561#[derive(Args, Debug)]
562pub struct VersionWorkspaceValidateCmd {
563 #[command(flatten)]
564 pub target: VersionWorkspaceTargetArgs,
565 #[arg(long, help = "Limit cargo validation to a single package name")]
566 pub package: Option<String>,
567 #[arg(long, help = "Run `cargo check -q` as part of validation")]
568 pub cargo_check: bool,
569 #[arg(
570 long,
571 help = "Run `cargo publish --dry-run --locked` for publishable packages"
572 )]
573 pub package_dry_run: bool,
574}
575
576#[derive(Args, Debug)]
577#[command(arg_required_else_help = true)]
578pub struct VersionWorkspacePublishCmd {
579 #[command(subcommand)]
580 pub command: VersionWorkspacePublishSubCommand,
581}
582
583#[derive(Subcommand, Debug)]
584pub enum VersionWorkspacePublishSubCommand {
585 #[command(about = "Show publish order, crates.io visibility, and blockers without publishing")]
586 Plan(VersionWorkspacePublishPlanCmd),
587 #[command(about = "Publish workspace packages in dependency order")]
588 Run(VersionWorkspacePublishRunCmd),
589}
590
591#[derive(Args, Debug)]
592pub struct VersionWorkspacePublishPlanCmd {
593 #[command(flatten)]
594 pub target: VersionWorkspaceTargetArgs,
595}
596
597#[derive(Args, Debug)]
598pub struct VersionWorkspacePublishRunCmd {
599 #[command(flatten)]
600 pub target: VersionWorkspaceTargetArgs,
601 #[arg(long, help = "Preview publish actions without calling cargo publish")]
602 pub dry_run: bool,
603 #[arg(
604 long,
605 help = "Start publishing from this package in the computed order"
606 )]
607 pub from: Option<String>,
608 #[arg(long, help = "Publish only this package")]
609 pub only: Option<String>,
610 #[arg(long, help = "Continue publishing remaining packages after a failure")]
611 pub continue_on_error: bool,
612 #[arg(long, help = "Allow publishing from a dirty worktree")]
613 pub allow_dirty: bool,
614 #[arg(
615 long,
616 default_value_t = 180.0,
617 help = "How long to wait for each published version to become visible on crates.io"
618 )]
619 pub timeout_seconds: f64,
620 #[arg(
621 long,
622 default_value_t = 5.0,
623 help = "How often to poll crates.io for the just-published version"
624 )]
625 pub poll_interval_seconds: f64,
626}
627
628#[derive(Args, Debug)]
629pub struct RedeployV2Cmd {
630 #[arg(short = 'p', long = "password")]
631 pub password: Option<String>,
632 #[arg(short = 'u', long = "username")]
633 pub username: Option<String>,
634 #[arg(short = 'h', long = "host")]
635 pub host: Option<String>,
636 #[arg(short = 'd', long = "project-dir")]
637 pub project_dir: Option<String>,
638}
639
640#[derive(Args, Debug)]
641pub struct LogsCmd {
642 #[arg()]
643 pub project: Option<String>,
644 #[arg(long = "ssh-host", help = "SSH host to stream logs from")]
645 pub ssh_host: Option<String>,
646 #[arg(long = "ssh-username", help = "SSH username for remote host")]
647 pub ssh_username: Option<String>,
648 #[arg(long = "ssh-password", help = "SSH password for remote host")]
649 pub ssh_password: Option<String>,
650}
651
652#[derive(Args, Debug)]
653pub struct SshCmd {
654 #[arg(long = "host", alias = "ssh-host", help = "SSH host or IP address")]
655 pub ssh_host: Option<String>,
656 #[arg(
657 long = "port",
658 default_value_t = 22,
659 help = "SSH port for direct connections"
660 )]
661 pub ssh_port: u16,
662 #[arg(
663 long = "username",
664 alias = "ssh-username",
665 help = "SSH username for the remote host"
666 )]
667 pub ssh_username: Option<String>,
668 #[arg(
669 long = "password",
670 alias = "ssh-password",
671 help = "SSH password (omit to use stored config or a secure prompt)"
672 )]
673 pub ssh_password: Option<String>,
674 #[arg(
675 long,
676 help = "Path to a private key file to use instead of password auth"
677 )]
678 pub private_key: Option<PathBuf>,
679 #[arg(long, help = "Passphrase for --private-key when required")]
680 pub private_key_passphrase: Option<String>,
681 #[arg(
682 long,
683 help = "Run this remote command in a PTY instead of opening the default login shell"
684 )]
685 pub command: Option<String>,
686 #[arg(
687 long,
688 help = "TERM value sent to the server (default: TERM env var or xterm-256color)"
689 )]
690 pub term: Option<String>,
691 #[arg(long, help = "Disable SSH host key verification")]
692 pub no_host_key_check: bool,
693 #[arg(
694 long,
695 help = "Pin the SSH host key as a base64 blob when using tunnels or first-connect flows"
696 )]
697 pub host_key: Option<String>,
698 #[arg(
699 long,
700 help = "Path to a known_hosts file used for SSH host verification"
701 )]
702 pub known_hosts_file: Option<PathBuf>,
703 #[arg(
704 long,
705 help = "Cloudflare Access hostname used to open a local cloudflared TCP forwarder"
706 )]
707 pub cloudflared_hostname: Option<String>,
708 #[arg(long, help = "Override the cloudflared binary path")]
709 pub cloudflared_binary: Option<PathBuf>,
710 #[arg(
711 long,
712 help = "Optional destination host:port passed to cloudflared access tcp"
713 )]
714 pub cloudflared_destination: Option<String>,
715}
716
717#[derive(Args, Debug)]
718#[command(arg_required_else_help = true)]
719pub struct CloudflaredCmd {
720 #[command(subcommand)]
721 pub command: CloudflaredSubCommand,
722}
723
724#[derive(Subcommand, Debug)]
725pub enum CloudflaredSubCommand {
726 #[command(about = "Start a local cloudflared Access TCP forwarder")]
727 Tcp(CloudflaredTcpCmd),
728}
729
730#[derive(Args, Debug)]
731#[command(
732 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"
733)]
734pub struct CloudflaredTcpCmd {
735 #[arg(long, help = "Protected Cloudflare Access hostname")]
736 pub hostname: Option<String>,
737 #[arg(
738 long,
739 help = "Local listener address for the forwarder (default: auto-allocate 127.0.0.1:<port>)"
740 )]
741 pub listener: Option<String>,
742 #[arg(
743 long,
744 help = "Optional destination host:port passed to cloudflared access tcp"
745 )]
746 pub destination: Option<String>,
747 #[arg(long, help = "Override the cloudflared binary path")]
748 pub binary: Option<PathBuf>,
749}
750
751#[derive(Args, Debug)]
752pub struct NginxCmd {
753 #[command(subcommand)]
754 pub command: NginxSubCommand,
755}
756
757#[derive(Args, Debug)]
758pub struct NetworkCmd {
759 #[command(subcommand)]
760 pub command: NetworkSubCommand,
761}
762
763#[derive(Subcommand, Debug)]
764pub enum NetworkSubCommand {
765 #[command(about = "Manage persistent floating IP configuration")]
766 FloatingIp(NetworkFloatingIpCmd),
767 #[command(about = "Inspect discovered network configuration sources")]
768 Config(NetworkConfigCmd),
769 #[command(about = "Manage Hetzner-specific Linux network configuration")]
770 Hetzner(NetworkHetznerCmd),
771}
772
773#[derive(Args, Debug)]
774pub struct NetworkFloatingIpCmd {
775 #[command(subcommand)]
776 pub command: NetworkFloatingIpSubCommand,
777}
778
779#[derive(Subcommand, Debug)]
780pub enum NetworkFloatingIpSubCommand {
781 #[command(about = "Add a persistent floating IP entry to detected network backend")]
782 Add {
783 #[arg(long, help = "Floating IP address (IPv4 or IPv6)")]
784 ip: String,
785 #[arg(long, help = "CIDR suffix (defaults: IPv4=32, IPv6=64)")]
786 cidr: Option<u8>,
787 #[arg(long, help = "Network interface override (auto-detected when omitted)")]
788 interface: Option<String>,
789 #[arg(long, help = "Optional label for backend metadata/file naming")]
790 label: Option<String>,
791 #[arg(long, help = "Apply network changes after writing config")]
792 apply: bool,
793 #[arg(long, help = "Preview computed changes without writing files")]
794 dry_run: bool,
795 },
796 #[command(about = "List floating IPs from runtime and persisted network config")]
797 List {
798 #[arg(long, help = "Emit JSON output")]
799 json: bool,
800 },
801}
802
803#[derive(Args, Debug)]
804pub struct NetworkConfigCmd {
805 #[command(subcommand)]
806 pub command: NetworkConfigSubCommand,
807}
808
809#[derive(Subcommand, Debug)]
810pub enum NetworkConfigSubCommand {
811 #[command(about = "List detected backend and configuration source files")]
812 List {
813 #[arg(long, help = "Emit JSON output")]
814 json: bool,
815 },
816}
817
818#[derive(Args, Debug)]
819pub struct NetworkHetznerCmd {
820 #[command(subcommand)]
821 pub command: NetworkHetznerSubCommand,
822}
823
824#[derive(Subcommand, Debug)]
825pub enum NetworkHetznerSubCommand {
826 #[command(about = "Configure a Hetzner vSwitch VLAN interface persistently")]
827 Vswitch(NetworkHetznerVswitchCmd),
828}
829
830#[derive(Args, Debug)]
831pub struct NetworkHetznerVswitchCmd {
832 #[command(subcommand)]
833 pub command: NetworkHetznerVswitchSubCommand,
834}
835
836#[derive(Subcommand, Debug)]
837pub enum NetworkHetznerVswitchSubCommand {
838 #[command(about = "Write persistent Linux config for a Hetzner vSwitch VLAN interface")]
839 Setup {
840 #[arg(
841 long,
842 help = "Private IPv4 address to assign on the vSwitch VLAN interface"
843 )]
844 ip: String,
845 #[arg(
846 long,
847 default_value_t = 24,
848 help = "CIDR prefix for --ip (default: 24)"
849 )]
850 cidr: u8,
851 #[arg(long, help = "Physical parent interface (auto-detected when omitted)")]
852 interface: Option<String>,
853 #[arg(long, help = "Hetzner vSwitch VLAN ID")]
854 vlan_id: u16,
855 #[arg(long, default_value_t = 1400, help = "Interface MTU (default: 1400)")]
856 mtu: u16,
857 #[arg(
858 long,
859 default_value = "10.0.3.1",
860 help = "Gateway for the routed Hetzner cloud network"
861 )]
862 gateway: String,
863 #[arg(
864 long,
865 default_value = "10.0.0.0/16",
866 help = "Destination CIDR routed through the Hetzner vSwitch gateway"
867 )]
868 route_cidr: String,
869 #[arg(long, help = "Apply or activate the new config immediately")]
870 apply: bool,
871 #[arg(long, help = "Preview file changes without writing them")]
872 dry_run: bool,
873 },
874}
875
876#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
877pub enum NginxDnsMode {
878 Manual,
879 Plugin,
880}
881
882#[derive(Subcommand, Debug)]
883pub enum NginxSubCommand {
884 #[command(
885 about = "Provision an HTTPS NGINX reverse proxy with Certbot",
886 long_about = "Provision an NGINX reverse proxy, issue or reuse Let's Encrypt certificates,\n\
887and write final HTTP->HTTPS redirect + TLS proxy config.\n\
888\n\
889Wildcard domains (for example *.example.com) require DNS-01 mode.\n\
890Use --dns-mode manual for interactive TXT record prompts, or --dns-mode plugin\n\
891with --dns-plugin and --dns-creds for non-interactive provider automation."
892 )]
893 Setup {
894 #[arg(short, long, help = "Domain name (supports wildcard: *.example.com)")]
895 domain: String,
896 #[arg(short, long, help = "Port to proxy to")]
897 port: u16,
898 #[arg(
899 short,
900 long,
901 help = "Email used for Let's Encrypt account registration"
902 )]
903 email: String,
904 #[arg(
905 long,
906 value_enum,
907 default_value_t = NginxDnsMode::Manual,
908 help = "DNS challenge mode for wildcard certificates: manual or plugin"
909 )]
910 dns_mode: NginxDnsMode,
911 #[arg(
912 long,
913 help = "Certbot DNS plugin name for --dns-mode plugin (for example: cloudflare)"
914 )]
915 dns_plugin: Option<String>,
916 #[arg(
917 long,
918 help = "Path to DNS plugin credentials file for --dns-mode plugin"
919 )]
920 dns_creds: Option<PathBuf>,
921 #[arg(
922 long,
923 default_value_t = true,
924 action = clap::ArgAction::Set,
925 value_parser = clap::builder::BoolishValueParser::new(),
926 help = "For wildcard domains, also request the base domain certificate (true|false)"
927 )]
928 include_base: bool,
929 },
930 #[command(about = "List discovered NGINX sites with listen/upstream ports")]
931 List,
932 #[command(about = "Show full NGINX config for one domain or all domains")]
933 Show {
934 #[arg(help = "Optional domain name to inspect")]
935 domain: Option<String>,
936 },
937 #[command(about = "Open an NGINX site config in your configured editor")]
938 Edit {
939 #[arg(help = "Domain name to edit")]
940 domain: String,
941 },
942 #[command(about = "Update upstream port for an existing NGINX site")]
943 Update {
944 #[arg(short, long, help = "Domain name to update")]
945 domain: String,
946 #[arg(short, long, help = "New port to proxy to")]
947 port: u16,
948 },
949}
950
951#[derive(Args, Debug)]
952pub struct DiagCmd {
953 #[arg(long, help = "Check Nginx configuration")]
954 pub nginx: bool,
955 #[arg(long, help = "Check specific ports (comma-separated)")]
956 pub ports: Option<String>,
957 #[arg(long, help = "Skip internet speed test")]
958 pub no_speed_test: bool,
959 #[arg(
960 long,
961 help = "Path to docker compose file to validate (defaults to docker-compose.yml/compose.yml)"
962 )]
963 pub compose_file: Option<String>,
964}
965
966#[derive(Args, Debug)]
967pub struct MonitorCmd {
968 #[command(subcommand)]
969 pub command: Option<MonitorSubCommand>,
970}
971
972#[derive(Subcommand, Debug)]
973pub enum MonitorSubCommand {
974 Check,
975 Start,
976}
977
978#[cfg(feature = "monitoring")]
979#[derive(Args, Debug)]
980pub struct MonitoringCmd {
981 #[command(subcommand)]
982 pub command: MonitoringSubCommand,
983}
984
985#[cfg(feature = "monitoring")]
986#[derive(Subcommand, Debug)]
987pub enum MonitoringSubCommand {
988 Serve {
989 #[arg(
990 short,
991 long,
992 default_value = "prodzilla.yml",
993 help = "Monitoring config file"
994 )]
995 file: String,
996 },
997 RunOnce {
998 #[arg(
999 short,
1000 long,
1001 default_value = "prodzilla.yml",
1002 help = "Monitoring config file"
1003 )]
1004 file: String,
1005 #[arg(long, help = "Run probes only")]
1006 probes_only: bool,
1007 #[arg(long, help = "Run stories only")]
1008 stories_only: bool,
1009 },
1010 List {
1011 #[arg(
1012 short,
1013 long,
1014 default_value = "prodzilla.yml",
1015 help = "Monitoring config file"
1016 )]
1017 file: String,
1018 },
1019}
1020
1021#[derive(Args, Debug)]
1022#[command(
1023 arg_required_else_help = true,
1024 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."
1025)]
1026pub struct ApiCmd {
1027 #[command(subcommand)]
1028 pub command: ApiSubCommand,
1029}
1030
1031#[derive(Args, Debug, Clone, Default)]
1032pub struct ApiTargetOptions {
1033 #[arg(long, help = "Override the request base URL for this command")]
1034 pub base_url: Option<String>,
1035 #[arg(
1036 long,
1037 help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
1038 )]
1039 pub web: bool,
1040 #[arg(
1041 long,
1042 help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
1043 )]
1044 pub no_auth: bool,
1045 #[arg(
1046 long,
1047 help = "Extra header in 'Name: Value' format",
1048 value_name = "HEADER"
1049 )]
1050 pub header: Vec<String>,
1051 #[arg(long, help = "Print response headers")]
1052 pub include_headers: bool,
1053 #[arg(
1054 long,
1055 help = "Print the response body as-is without JSON pretty formatting"
1056 )]
1057 pub raw: bool,
1058}
1059
1060#[cfg(feature = "docker")]
1061#[derive(Args, Debug)]
1062pub struct DockerCmd {
1063 #[arg(
1064 trailing_var_arg = true,
1065 allow_hyphen_values = true,
1066 help = "Arguments to pass directly to the Docker CLI (default: --help)"
1067 )]
1068 pub args: Vec<String>,
1069}
1070
1071#[derive(Subcommand, Debug)]
1072pub enum ApiSubCommand {
1073 #[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
1074 Install {
1075 #[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
1076 port: u16,
1077 },
1078 #[command(about = "Call the XBP API health endpoint")]
1079 Health(ApiHealthCmd),
1080 #[command(about = "Manage XBP control-plane projects")]
1081 Projects(ApiProjectsCmd),
1082 #[command(about = "Manage XBP daemon registrations and heartbeats")]
1083 Daemons(ApiDaemonsCmd),
1084 #[command(about = "Manage XBP deployment jobs")]
1085 Jobs(ApiJobsCmd),
1086 #[command(about = "Manage XBP proxy routes on the local API server")]
1087 Routes(ApiRoutesCmd),
1088 #[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
1089 Request(ApiRequestCmd),
1090}
1091
1092#[derive(Args, Debug)]
1093pub struct ApiHealthCmd {
1094 #[command(flatten)]
1095 pub target: ApiTargetOptions,
1096}
1097
1098#[derive(Args, Debug)]
1099pub struct ApiProjectsCmd {
1100 #[command(subcommand)]
1101 pub command: ApiProjectsSubCommand,
1102}
1103
1104#[derive(Subcommand, Debug)]
1105pub enum ApiProjectsSubCommand {
1106 #[command(about = "List projects from the XBP control-plane API")]
1107 List(ApiProjectsListCmd),
1108 #[command(about = "Create or upsert a control-plane project")]
1109 Create(Box<ApiProjectsCreateCmd>),
1110}
1111
1112#[derive(Args, Debug)]
1113pub struct ApiProjectsListCmd {
1114 #[arg(long, help = "Optional organization ID filter")]
1115 pub organization_id: Option<String>,
1116 #[command(flatten)]
1117 pub target: ApiTargetOptions,
1118}
1119
1120#[derive(Args, Debug)]
1121pub struct ApiProjectsCreateCmd {
1122 #[arg(long, help = "Project name")]
1123 pub name: String,
1124 #[arg(long, help = "Project path or repo path key")]
1125 pub path: String,
1126 #[arg(long, help = "Optional organization ID")]
1127 pub organization_id: Option<String>,
1128 #[arg(long, help = "Optional project slug")]
1129 pub slug: Option<String>,
1130 #[arg(long, help = "Optional project version")]
1131 pub version: Option<String>,
1132 #[arg(long, help = "Optional build directory")]
1133 pub build_dir: Option<String>,
1134 #[arg(long, help = "Optional runtime enum value")]
1135 pub runtime: Option<String>,
1136 #[arg(long, help = "Optional default branch")]
1137 pub default_branch: Option<String>,
1138 #[arg(long, help = "Optional repository root directory")]
1139 pub root_directory: Option<String>,
1140 #[arg(long, help = "Optional build command")]
1141 pub build_command: Option<String>,
1142 #[arg(long, help = "Optional install command")]
1143 pub install_command: Option<String>,
1144 #[arg(long, help = "Optional start command")]
1145 pub start_command: Option<String>,
1146 #[arg(long, help = "Optional output directory")]
1147 pub output_directory: Option<String>,
1148 #[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
1149 pub repository_json: Option<String>,
1150 #[arg(long, help = "Runtime policy JSON payload")]
1151 pub runtime_policy_json: Option<String>,
1152 #[arg(long, help = "Metadata JSON object")]
1153 pub metadata_json: Option<String>,
1154 #[command(flatten)]
1155 pub target: ApiTargetOptions,
1156}
1157
1158#[derive(Args, Debug)]
1159pub struct ApiDaemonsCmd {
1160 #[command(subcommand)]
1161 pub command: ApiDaemonsSubCommand,
1162}
1163
1164#[derive(Subcommand, Debug)]
1165pub enum ApiDaemonsSubCommand {
1166 #[command(about = "List registered daemons")]
1167 List(ApiDaemonsListCmd),
1168 #[command(about = "Register or upsert a daemon record")]
1169 Register(ApiDaemonsRegisterCmd),
1170 #[command(about = "Post a heartbeat update for a daemon")]
1171 Heartbeat(ApiDaemonsHeartbeatCmd),
1172 #[command(about = "Update daemon status only")]
1173 UpdateStatus(ApiDaemonsUpdateStatusCmd),
1174}
1175
1176#[derive(Args, Debug)]
1177pub struct ApiDaemonsListCmd {
1178 #[command(flatten)]
1179 pub target: ApiTargetOptions,
1180}
1181
1182#[derive(Args, Debug)]
1183pub struct ApiDaemonsRegisterCmd {
1184 #[arg(long, help = "Daemon node name")]
1185 pub node_name: String,
1186 #[arg(long, help = "Daemon hostname")]
1187 pub hostname: String,
1188 #[arg(long, help = "Daemon binary version")]
1189 pub version: String,
1190 #[arg(long, help = "Optional region")]
1191 pub region: Option<String>,
1192 #[arg(long, help = "Optional public IP")]
1193 pub public_ip: Option<String>,
1194 #[arg(long, help = "Optional internal IP")]
1195 pub internal_ip: Option<String>,
1196 #[arg(long, help = "Optional status enum value")]
1197 pub status: Option<String>,
1198 #[arg(long, help = "Optional CPU core count")]
1199 pub cpu_cores: Option<i32>,
1200 #[arg(long, help = "Optional total memory in MB")]
1201 pub memory_total_mb: Option<i32>,
1202 #[arg(long, help = "Optional total disk in GB")]
1203 pub disk_total_gb: Option<i32>,
1204 #[arg(long, help = "Labels JSON object")]
1205 pub labels_json: Option<String>,
1206 #[arg(long, help = "Metadata JSON object")]
1207 pub metadata_json: Option<String>,
1208 #[command(flatten)]
1209 pub target: ApiTargetOptions,
1210}
1211
1212#[derive(Args, Debug)]
1213pub struct ApiDaemonsHeartbeatCmd {
1214 #[arg(help = "Daemon ID")]
1215 pub daemon_id: String,
1216 #[arg(long, help = "Optional status enum value")]
1217 pub status: Option<String>,
1218 #[arg(long, help = "Optional daemon version")]
1219 pub version: Option<String>,
1220 #[arg(long, help = "Optional public IP")]
1221 pub public_ip: Option<String>,
1222 #[arg(long, help = "Optional internal IP")]
1223 pub internal_ip: Option<String>,
1224 #[arg(long, help = "Optional CPU core count")]
1225 pub cpu_cores: Option<i32>,
1226 #[arg(long, help = "Optional total memory in MB")]
1227 pub memory_total_mb: Option<i32>,
1228 #[arg(long, help = "Optional total disk in GB")]
1229 pub disk_total_gb: Option<i32>,
1230 #[arg(long, help = "Labels JSON object")]
1231 pub labels_json: Option<String>,
1232 #[command(flatten)]
1233 pub target: ApiTargetOptions,
1234}
1235
1236#[derive(Args, Debug)]
1237pub struct ApiDaemonsUpdateStatusCmd {
1238 #[arg(help = "Daemon ID")]
1239 pub daemon_id: String,
1240 #[arg(long, help = "Daemon status enum value")]
1241 pub status: String,
1242 #[command(flatten)]
1243 pub target: ApiTargetOptions,
1244}
1245
1246#[derive(Args, Debug)]
1247pub struct ApiJobsCmd {
1248 #[command(subcommand)]
1249 pub command: ApiJobsSubCommand,
1250}
1251
1252#[derive(Subcommand, Debug)]
1253pub enum ApiJobsSubCommand {
1254 #[command(about = "List deployment jobs")]
1255 List(ApiJobsListCmd),
1256 #[command(about = "Create a deployment job for a project")]
1257 Create(ApiJobsCreateCmd),
1258 #[command(about = "Claim the next deployment job for a daemon")]
1259 Claim(ApiJobsClaimCmd),
1260 #[command(about = "Update deployment job status")]
1261 Update(ApiJobsUpdateCmd),
1262}
1263
1264#[derive(Args, Debug)]
1265pub struct ApiJobsListCmd {
1266 #[arg(long, help = "Optional project ID filter")]
1267 pub project_id: Option<String>,
1268 #[arg(long, help = "Optional deployment ID filter")]
1269 pub deployment_id: Option<String>,
1270 #[arg(long, help = "Optional daemon ID filter")]
1271 pub daemon_id: Option<String>,
1272 #[arg(long, help = "Optional status filter")]
1273 pub status: Option<String>,
1274 #[arg(long, help = "Optional result limit")]
1275 pub limit: Option<usize>,
1276 #[command(flatten)]
1277 pub target: ApiTargetOptions,
1278}
1279
1280#[derive(Args, Debug)]
1281pub struct ApiJobsCreateCmd {
1282 #[arg(long, help = "Project ID")]
1283 pub project_id: String,
1284 #[arg(long, help = "Deployment ID")]
1285 pub deployment_id: String,
1286 #[arg(long, help = "Optional daemon ID assignment")]
1287 pub daemon_id: Option<String>,
1288 #[arg(long, help = "Optional priority")]
1289 pub priority: Option<i32>,
1290 #[arg(long, help = "Optional max attempts")]
1291 pub max_attempts: Option<i32>,
1292 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1293 pub run_after: Option<String>,
1294 #[arg(long, help = "Optional payload JSON object")]
1295 pub payload_json: Option<String>,
1296 #[command(flatten)]
1297 pub target: ApiTargetOptions,
1298}
1299
1300#[derive(Args, Debug)]
1301pub struct ApiJobsClaimCmd {
1302 #[arg(long, help = "Daemon ID claiming work")]
1303 pub daemon_id: String,
1304 #[arg(long, help = "Optional lock owner")]
1305 pub locked_by: Option<String>,
1306 #[command(flatten)]
1307 pub target: ApiTargetOptions,
1308}
1309
1310#[derive(Args, Debug)]
1311pub struct ApiJobsUpdateCmd {
1312 #[arg(help = "Deployment job ID")]
1313 pub job_id: String,
1314 #[arg(long, help = "Deployment job status enum value")]
1315 pub status: String,
1316 #[arg(long, help = "Optional error text")]
1317 pub error_text: Option<String>,
1318 #[command(flatten)]
1319 pub target: ApiTargetOptions,
1320}
1321
1322#[derive(Args, Debug)]
1323pub struct ApiRoutesCmd {
1324 #[command(subcommand)]
1325 pub command: ApiRoutesSubCommand,
1326}
1327
1328#[derive(Subcommand, Debug)]
1329pub enum ApiRoutesSubCommand {
1330 #[command(about = "List configured proxy routes")]
1331 List(ApiRoutesListCmd),
1332 #[command(about = "Create or replace a proxy route")]
1333 Create(ApiRoutesCreateCmd),
1334 #[command(about = "Delete a proxy route by domain")]
1335 Delete(ApiRoutesDeleteCmd),
1336}
1337
1338#[derive(Args, Debug)]
1339pub struct ApiRoutesListCmd {
1340 #[command(flatten)]
1341 pub target: ApiTargetOptions,
1342}
1343
1344#[derive(Args, Debug)]
1345pub struct ApiRoutesCreateCmd {
1346 #[arg(long, help = "Domain name for the route")]
1347 pub domain: String,
1348 #[arg(long, help = "Upstream target URL", required = true)]
1349 pub target: Vec<String>,
1350 #[arg(
1351 long,
1352 help = "Weighted upstream target in url=weight form",
1353 value_name = "URL=WEIGHT"
1354 )]
1355 pub weighted_target: Vec<String>,
1356 #[arg(long, help = "Optional header condition")]
1357 pub header_condition: Option<String>,
1358 #[arg(long, help = "Optional path prefix condition")]
1359 pub path_prefix: Option<String>,
1360 #[command(flatten)]
1361 pub target_options: ApiTargetOptions,
1362}
1363
1364#[derive(Args, Debug)]
1365pub struct ApiRoutesDeleteCmd {
1366 #[arg(help = "Domain name for the route")]
1367 pub domain: String,
1368 #[command(flatten)]
1369 pub target: ApiTargetOptions,
1370}
1371
1372#[derive(Args, Debug)]
1373pub struct ApiRequestCmd {
1374 #[arg(help = "Request path like /projects or a full https:// URL")]
1375 pub path: String,
1376 #[arg(
1377 short = 'X',
1378 long,
1379 help = "HTTP method to use (default: GET, or POST when a body is provided)"
1380 )]
1381 pub method: Option<String>,
1382 #[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
1383 pub body: Option<String>,
1384 #[arg(long, help = "Read the request body from a file")]
1385 pub body_file: Option<PathBuf>,
1386 #[command(flatten)]
1387 pub target: ApiTargetOptions,
1388}
1389#[derive(Args, Debug)]
1390pub struct TailCmd {
1391 #[arg(long, help = "Tail Kafka topic instead of log files")]
1392 pub kafka: bool,
1393 #[arg(long, help = "Ship logs to Kafka")]
1394 pub ship: bool,
1395}
1396
1397#[derive(Args, Debug)]
1398pub struct GenerateCmd {
1399 #[command(subcommand)]
1400 pub command: GenerateSubCommand,
1401}
1402
1403#[derive(Subcommand, Debug)]
1404pub enum GenerateSubCommand {
1405 #[command(about = "Generate or update .xbp/xbp.yaml (and convert legacy JSON)")]
1406 Config(GenerateConfigCmd),
1407 Systemd(GenerateSystemdCmd),
1408}
1409
1410#[derive(Args, Debug)]
1411pub struct GenerateConfigCmd {
1412 #[arg(
1413 long,
1414 help = "Overwrite .xbp/xbp.yaml if it already exists (default errors when present)"
1415 )]
1416 pub force: bool,
1417 #[arg(
1418 long,
1419 help = "Refresh .xbp/xbp.yaml by applying project detection defaults for missing fields"
1420 )]
1421 pub update: bool,
1422 #[arg(
1423 long,
1424 help = "Path to a legacy xbp.json file to convert into .xbp/xbp.yaml"
1425 )]
1426 pub from_json: Option<PathBuf>,
1427}
1428
1429#[cfg(feature = "secrets")]
1430#[derive(Args, Debug)]
1431pub struct SecretsCmd {
1432 #[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
1433 pub provider: SecretsProviderKind,
1434 #[arg(long, help = "GitHub repository override (owner/repo)")]
1435 pub repo: Option<String>,
1436 #[arg(
1437 long,
1438 help = "Provider token override (GitHub token or Cloudflare API token)"
1439 )]
1440 pub token: Option<String>,
1441 #[arg(long, help = "Cloudflare account ID override")]
1442 pub account_id: Option<String>,
1443 #[arg(
1444 long = "environment",
1445 alias = "env",
1446 value_enum,
1447 default_value_t = SecretsEnvironment::XbpDev,
1448 help = "GitHub Actions environment to sync (default: xbp-dev)"
1449 )]
1450 pub environment: SecretsEnvironment,
1451 #[command(subcommand)]
1452 pub command: Option<SecretsSubCommand>,
1453}
1454
1455#[cfg(feature = "secrets")]
1456#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
1457pub enum SecretsProviderKind {
1458 Github,
1459 Cloudflare,
1460}
1461
1462#[cfg(feature = "secrets")]
1463#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
1464pub enum SecretsEnvironment {
1465 #[value(name = "xbp-dev")]
1466 XbpDev,
1467 #[value(name = "xbp-preview")]
1468 XbpPreview,
1469 #[value(name = "xbp-prod")]
1470 XbpProd,
1471}
1472
1473#[cfg(feature = "secrets")]
1474impl SecretsEnvironment {
1475 pub fn as_str(self) -> &'static str {
1476 match self {
1477 Self::XbpDev => "xbp-dev",
1478 Self::XbpPreview => "xbp-preview",
1479 Self::XbpProd => "xbp-prod",
1480 }
1481 }
1482}
1483
1484#[cfg(feature = "secrets")]
1485#[derive(Subcommand, Debug)]
1486pub enum SecretsSubCommand {
1487 #[command(alias = "ls", alias = "list-providers")]
1489 Providers,
1490 List(ListCmd),
1492 Push(PushCmd),
1494 Pull(PullCmd),
1496 GenerateDefault(GenerateDefaultCmd),
1498 GenerateExample(GenerateExampleCmd),
1500 Diff,
1502 Verify,
1504 #[command(name = "diag", alias = "doctor")]
1506 Diag,
1507 Stores(SecretsStoresCmd),
1509 Secrets(CloudflareSecretsCmd),
1511 Quota(SecretsQuotaCmd),
1513 #[command(name = "usage")]
1515 Usage,
1516}
1517
1518#[cfg(feature = "secrets")]
1519#[derive(Args, Debug)]
1520pub struct ListCmd {
1521 #[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
1522 pub file: Option<String>,
1523 #[arg(long, help = "Output format: plain (default) or json")]
1524 pub format: Option<String>,
1525}
1526
1527#[cfg(feature = "secrets")]
1528#[derive(Args, Debug)]
1529pub struct PushCmd {
1530 #[arg(long, help = "Path to env file (default: .env.local/.env)")]
1531 pub file: Option<String>,
1532 #[arg(
1533 long,
1534 help = "Force overwrite existing GitHub Actions environment variables"
1535 )]
1536 pub force: bool,
1537 #[arg(long, help = "Show what would be pushed without making changes")]
1538 pub dry_run: bool,
1539}
1540
1541#[cfg(feature = "secrets")]
1542#[derive(Args, Debug)]
1543pub struct PullCmd {
1544 #[arg(long, help = "Output file path (default: .env.local)")]
1545 pub output: Option<String>,
1546}
1547
1548#[cfg(feature = "secrets")]
1549#[derive(Args, Debug)]
1550pub struct GenerateDefaultCmd {
1551 #[arg(long, help = "Output file path (default: .env.default)")]
1552 pub output: Option<String>,
1553}
1554
1555#[cfg(feature = "secrets")]
1556#[derive(Args, Debug)]
1557pub struct GenerateExampleCmd {
1558 #[arg(long, help = "Output file path (default: .env.example)")]
1559 pub output: Option<String>,
1560 #[arg(long, help = "Remove keys from .env.local not in .env.example")]
1561 pub clean: bool,
1562 #[arg(long, help = "Only include vars matching prefix (repeatable)")]
1563 pub include_prefix: Vec<String>,
1564 #[arg(long, help = "Exclude vars matching prefix (repeatable)")]
1565 pub exclude_prefix: Vec<String>,
1566}
1567
1568#[cfg(feature = "secrets")]
1569#[derive(Args, Debug)]
1570pub struct SecretsStoresCmd {
1571 #[command(subcommand)]
1572 pub command: SecretsStoresSubCommand,
1573}
1574
1575#[cfg(feature = "secrets")]
1576#[derive(Subcommand, Debug)]
1577pub enum SecretsStoresSubCommand {
1578 List(CloudflareSecretsStoreListCmd),
1579 Get(CloudflareSecretsStoreGetCmd),
1580 Create(CloudflareSecretsStoreCreateCmd),
1581 Delete(CloudflareSecretsStoreDeleteCmd),
1582}
1583
1584#[cfg(feature = "secrets")]
1585#[derive(Args, Debug)]
1586pub struct CloudflareSecretsStoreListCmd {}
1587
1588#[cfg(feature = "secrets")]
1589#[derive(Args, Debug)]
1590pub struct CloudflareSecretsStoreGetCmd {
1591 #[arg(long)]
1592 pub store_id: String,
1593}
1594
1595#[cfg(feature = "secrets")]
1596#[derive(Args, Debug)]
1597pub struct CloudflareSecretsStoreCreateCmd {
1598 #[arg(long)]
1599 pub name: String,
1600}
1601
1602#[cfg(feature = "secrets")]
1603#[derive(Args, Debug)]
1604pub struct CloudflareSecretsStoreDeleteCmd {
1605 #[arg(long)]
1606 pub store_id: String,
1607}
1608
1609#[cfg(feature = "secrets")]
1610#[derive(Args, Debug)]
1611pub struct CloudflareSecretsCmd {
1612 #[command(subcommand)]
1613 pub command: CloudflareSecretsSubCommand,
1614}
1615
1616#[cfg(feature = "secrets")]
1617#[derive(Subcommand, Debug)]
1618pub enum CloudflareSecretsSubCommand {
1619 List(CloudflareSecretsListCmd),
1620 Get(CloudflareSecretsGetCmd),
1621 Create(CloudflareSecretsCreateCmd),
1622 Edit(CloudflareSecretsEditCmd),
1623 Delete(CloudflareSecretsDeleteCmd),
1624 #[command(name = "delete-bulk")]
1625 DeleteBulk(CloudflareSecretsBulkDeleteCmd),
1626 Duplicate(CloudflareSecretsDuplicateCmd),
1627}
1628
1629#[cfg(feature = "secrets")]
1630#[derive(Args, Debug)]
1631pub struct CloudflareSecretsListCmd {
1632 #[arg(long)]
1633 pub store_id: String,
1634}
1635
1636#[cfg(feature = "secrets")]
1637#[derive(Args, Debug)]
1638pub struct CloudflareSecretsGetCmd {
1639 #[arg(long)]
1640 pub store_id: String,
1641 #[arg(long)]
1642 pub secret_id: String,
1643}
1644
1645#[cfg(feature = "secrets")]
1646#[derive(Args, Debug)]
1647pub struct CloudflareSecretsCreateCmd {
1648 #[arg(long)]
1649 pub store_id: String,
1650 #[arg(long)]
1651 pub name: String,
1652 #[arg(long)]
1653 pub value: String,
1654 #[arg(long, value_delimiter = ',')]
1655 pub scopes: Vec<String>,
1656 #[arg(long)]
1657 pub comment: Option<String>,
1658}
1659
1660#[cfg(feature = "secrets")]
1661#[derive(Args, Debug)]
1662pub struct CloudflareSecretsEditCmd {
1663 #[arg(long)]
1664 pub store_id: String,
1665 #[arg(long)]
1666 pub secret_id: String,
1667 #[arg(long)]
1668 pub name: Option<String>,
1669 #[arg(long)]
1670 pub value: Option<String>,
1671 #[arg(long, value_delimiter = ',')]
1672 pub scopes: Vec<String>,
1673 #[arg(long)]
1674 pub comment: Option<String>,
1675}
1676
1677#[cfg(feature = "secrets")]
1678#[derive(Args, Debug)]
1679pub struct CloudflareSecretsDeleteCmd {
1680 #[arg(long)]
1681 pub store_id: String,
1682 #[arg(long)]
1683 pub secret_id: String,
1684}
1685
1686#[cfg(feature = "secrets")]
1687#[derive(Args, Debug)]
1688pub struct CloudflareSecretsBulkDeleteCmd {
1689 #[arg(long)]
1690 pub store_id: String,
1691 #[arg(long = "secret-id", required = true)]
1692 pub secret_ids: Vec<String>,
1693}
1694
1695#[cfg(feature = "secrets")]
1696#[derive(Args, Debug)]
1697pub struct CloudflareSecretsDuplicateCmd {
1698 #[arg(long)]
1699 pub store_id: String,
1700 #[arg(long)]
1701 pub secret_id: String,
1702 #[arg(long)]
1703 pub name: String,
1704 #[arg(long, value_delimiter = ',')]
1705 pub scopes: Vec<String>,
1706 #[arg(long)]
1707 pub comment: Option<String>,
1708}
1709
1710#[cfg(feature = "secrets")]
1711#[derive(Args, Debug)]
1712pub struct SecretsQuotaCmd {
1713 #[command(subcommand)]
1714 pub command: SecretsQuotaSubCommand,
1715}
1716
1717#[cfg(feature = "secrets")]
1718#[derive(Subcommand, Debug)]
1719pub enum SecretsQuotaSubCommand {
1720 Get(SecretsQuotaGetCmd),
1721}
1722
1723#[cfg(feature = "secrets")]
1724#[derive(Args, Debug)]
1725pub struct SecretsQuotaGetCmd {}
1726
1727#[derive(Args, Debug)]
1728pub struct DnsCmd {
1729 #[command(subcommand)]
1730 pub command: DnsSubCommand,
1731}
1732
1733#[derive(Subcommand, Debug)]
1734pub enum DnsSubCommand {
1735 #[command(alias = "ls", alias = "list")]
1736 Providers,
1737 Zones(DnsZonesCmd),
1738 Records(DnsRecordsCmd),
1739 Dnssec(DnssecCmd),
1740 Settings(DnsSettingsCmd),
1741}
1742
1743#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
1744pub enum DnsProviderKind {
1745 Cloudflare,
1746 Hetzner,
1747 Vercel,
1748 Custom,
1749}
1750
1751#[derive(Args, Debug)]
1752pub struct DnsZonesCmd {
1753 #[command(subcommand)]
1754 pub command: DnsZonesSubCommand,
1755}
1756
1757#[derive(Subcommand, Debug)]
1758pub enum DnsZonesSubCommand {
1759 List(DnsZoneListCmd),
1760 Get(DnsZoneGetCmd),
1761 Create(DnsZoneCreateCmd),
1762 Edit(DnsZoneEditCmd),
1763 Delete(DnsZoneDeleteCmd),
1764}
1765
1766#[derive(Args, Debug)]
1767pub struct DnsZoneListCmd {
1768 #[arg(long, value_enum)]
1769 pub provider: DnsProviderKind,
1770 #[arg(long)]
1771 pub account_id: Option<String>,
1772 #[arg(long)]
1773 pub account_name: Option<String>,
1774 #[arg(long = "account-name-op")]
1775 pub account_name_op: Option<String>,
1776 #[arg(long)]
1777 pub name: Option<String>,
1778 #[arg(long = "name-op")]
1779 pub name_op: Option<String>,
1780 #[arg(long)]
1781 pub status: Option<String>,
1782 #[arg(long = "type", value_delimiter = ',')]
1783 pub zone_types: Vec<String>,
1784 #[arg(long)]
1785 pub r#match: Option<String>,
1786 #[arg(long)]
1787 pub order: Option<String>,
1788 #[arg(long)]
1789 pub direction: Option<String>,
1790 #[arg(long)]
1791 pub page: Option<u64>,
1792 #[arg(long = "per-page")]
1793 pub per_page: Option<u64>,
1794 #[arg(long)]
1795 pub token: Option<String>,
1796}
1797
1798#[derive(Args, Debug)]
1799pub struct DnsZoneGetCmd {
1800 #[arg(long, value_enum)]
1801 pub provider: DnsProviderKind,
1802 #[arg(long)]
1803 pub zone_id: String,
1804 #[arg(long)]
1805 pub token: Option<String>,
1806}
1807
1808#[derive(Args, Debug)]
1809pub struct DnsZoneCreateCmd {
1810 #[arg(long, value_enum)]
1811 pub provider: DnsProviderKind,
1812 #[arg(long)]
1813 pub name: String,
1814 #[arg(long)]
1815 pub account_id: Option<String>,
1816 #[arg(long)]
1817 pub jump_start: bool,
1818 #[arg(long = "type")]
1819 pub zone_type: Option<String>,
1820 #[arg(long)]
1821 pub token: Option<String>,
1822}
1823
1824#[derive(Args, Debug)]
1825pub struct DnsZoneEditCmd {
1826 #[arg(long, value_enum)]
1827 pub provider: DnsProviderKind,
1828 #[arg(long)]
1829 pub zone_id: String,
1830 #[arg(long)]
1831 pub paused: Option<bool>,
1832 #[arg(long = "type")]
1833 pub zone_type: Option<String>,
1834 #[arg(long = "vanity-name-server")]
1835 pub vanity_name_servers: Vec<String>,
1836 #[arg(long)]
1837 pub token: Option<String>,
1838}
1839
1840#[derive(Args, Debug)]
1841pub struct DnsZoneDeleteCmd {
1842 #[arg(long, value_enum)]
1843 pub provider: DnsProviderKind,
1844 #[arg(long)]
1845 pub zone_id: String,
1846 #[arg(long)]
1847 pub token: Option<String>,
1848}
1849
1850#[derive(Args, Debug)]
1851pub struct DnsRecordsCmd {
1852 #[command(subcommand)]
1853 pub command: DnsRecordsSubCommand,
1854}
1855
1856#[derive(Subcommand, Debug)]
1857pub enum DnsRecordsSubCommand {
1858 List(DnsRecordListCmd),
1859 Get(DnsRecordGetCmd),
1860 Create(DnsRecordCreateCmd),
1861 Replace(DnsRecordReplaceCmd),
1862 Edit(DnsRecordEditCmd),
1863 Delete(DnsRecordDeleteCmd),
1864 Batch(DnsRecordBatchCmd),
1865 Import(DnsRecordImportCmd),
1866 Export(DnsRecordExportCmd),
1867}
1868
1869#[derive(Args, Debug)]
1870pub struct DnsRecordListCmd {
1871 #[arg(long, value_enum)]
1872 pub provider: DnsProviderKind,
1873 #[arg(long)]
1874 pub zone_id: String,
1875 #[arg(long = "type")]
1876 pub record_type: Option<String>,
1877 #[arg(long)]
1878 pub name: Option<String>,
1879 #[arg(long)]
1880 pub page: Option<u64>,
1881 #[arg(long = "per-page")]
1882 pub per_page: Option<u64>,
1883 #[arg(long)]
1884 pub token: Option<String>,
1885}
1886
1887#[derive(Args, Debug)]
1888pub struct DnsRecordGetCmd {
1889 #[arg(long, value_enum)]
1890 pub provider: DnsProviderKind,
1891 #[arg(long)]
1892 pub zone_id: String,
1893 #[arg(long)]
1894 pub record_id: String,
1895 #[arg(long)]
1896 pub token: Option<String>,
1897}
1898
1899#[derive(Args, Debug)]
1900pub struct DnsRecordCreateCmd {
1901 #[arg(long, value_enum)]
1902 pub provider: DnsProviderKind,
1903 #[arg(long)]
1904 pub zone_id: String,
1905 #[arg(long = "type")]
1906 pub record_type: String,
1907 #[arg(long)]
1908 pub name: String,
1909 #[arg(long)]
1910 pub content: String,
1911 #[arg(long)]
1912 pub ttl: Option<u32>,
1913 #[arg(long)]
1914 pub proxied: Option<bool>,
1915 #[arg(long)]
1916 pub priority: Option<u32>,
1917 #[arg(long)]
1918 pub comment: Option<String>,
1919 #[arg(long = "tag")]
1920 pub tags: Vec<String>,
1921 #[arg(long = "data-json")]
1922 pub data_json: Option<String>,
1923 #[arg(long = "settings-json")]
1924 pub settings_json: Option<String>,
1925 #[arg(long)]
1926 pub token: Option<String>,
1927}
1928
1929#[derive(Args, Debug)]
1930pub struct DnsRecordReplaceCmd {
1931 #[command(flatten)]
1932 pub common: DnsRecordCreateCmd,
1933 #[arg(long)]
1934 pub record_id: String,
1935}
1936
1937#[derive(Args, Debug)]
1938pub struct DnsRecordEditCmd {
1939 #[arg(long, value_enum)]
1940 pub provider: DnsProviderKind,
1941 #[arg(long)]
1942 pub zone_id: String,
1943 #[arg(long)]
1944 pub record_id: String,
1945 #[arg(long = "type")]
1946 pub record_type: Option<String>,
1947 #[arg(long)]
1948 pub name: Option<String>,
1949 #[arg(long)]
1950 pub content: Option<String>,
1951 #[arg(long)]
1952 pub ttl: Option<u32>,
1953 #[arg(long)]
1954 pub proxied: Option<bool>,
1955 #[arg(long)]
1956 pub priority: Option<u32>,
1957 #[arg(long)]
1958 pub comment: Option<String>,
1959 #[arg(long = "tag")]
1960 pub tags: Vec<String>,
1961 #[arg(long = "data-json")]
1962 pub data_json: Option<String>,
1963 #[arg(long = "settings-json")]
1964 pub settings_json: Option<String>,
1965 #[arg(long)]
1966 pub token: Option<String>,
1967}
1968
1969#[derive(Args, Debug)]
1970pub struct DnsRecordDeleteCmd {
1971 #[arg(long, value_enum)]
1972 pub provider: DnsProviderKind,
1973 #[arg(long)]
1974 pub zone_id: String,
1975 #[arg(long)]
1976 pub record_id: String,
1977 #[arg(long)]
1978 pub token: Option<String>,
1979}
1980
1981#[derive(Args, Debug)]
1982pub struct DnsRecordBatchCmd {
1983 #[arg(long, value_enum)]
1984 pub provider: DnsProviderKind,
1985 #[arg(long)]
1986 pub zone_id: String,
1987 #[arg(long)]
1988 pub input: PathBuf,
1989 #[arg(long)]
1990 pub token: Option<String>,
1991}
1992
1993#[derive(Args, Debug)]
1994pub struct DnsRecordImportCmd {
1995 #[arg(long, value_enum)]
1996 pub provider: DnsProviderKind,
1997 #[arg(long)]
1998 pub zone_id: String,
1999 #[arg(long)]
2000 pub file: PathBuf,
2001 #[arg(long)]
2002 pub token: Option<String>,
2003}
2004
2005#[derive(Args, Debug)]
2006pub struct DnsRecordExportCmd {
2007 #[arg(long, value_enum)]
2008 pub provider: DnsProviderKind,
2009 #[arg(long)]
2010 pub zone_id: String,
2011 #[arg(long)]
2012 pub output: Option<PathBuf>,
2013 #[arg(long)]
2014 pub token: Option<String>,
2015}
2016
2017#[derive(Args, Debug)]
2018pub struct DnssecCmd {
2019 #[command(subcommand)]
2020 pub command: DnssecSubCommand,
2021}
2022
2023#[derive(Subcommand, Debug)]
2024pub enum DnssecSubCommand {
2025 Get(DnssecGetCmd),
2026 Edit(DnssecEditCmd),
2027}
2028
2029#[derive(Args, Debug)]
2030pub struct DnssecGetCmd {
2031 #[arg(long, value_enum)]
2032 pub provider: DnsProviderKind,
2033 #[arg(long)]
2034 pub zone_id: String,
2035 #[arg(long)]
2036 pub token: Option<String>,
2037}
2038
2039#[derive(Args, Debug)]
2040pub struct DnssecEditCmd {
2041 #[arg(long, value_enum)]
2042 pub provider: DnsProviderKind,
2043 #[arg(long)]
2044 pub zone_id: String,
2045 #[arg(long)]
2046 pub status: Option<String>,
2047 #[arg(long = "dnssec-multi-signer")]
2048 pub dnssec_multi_signer: Option<bool>,
2049 #[arg(long = "dnssec-presigned")]
2050 pub dnssec_presigned: Option<bool>,
2051 #[arg(long = "dnssec-use-nsec3")]
2052 pub dnssec_use_nsec3: Option<bool>,
2053 #[arg(long)]
2054 pub token: Option<String>,
2055}
2056
2057#[derive(Args, Debug)]
2058pub struct DnsSettingsCmd {
2059 #[command(subcommand)]
2060 pub command: DnsSettingsSubCommand,
2061}
2062
2063#[derive(Subcommand, Debug)]
2064pub enum DnsSettingsSubCommand {
2065 Get(DnsSettingsGetCmd),
2066 Edit(DnsSettingsEditCmd),
2067}
2068
2069#[derive(Args, Debug)]
2070pub struct DnsSettingsGetCmd {
2071 #[arg(long, value_enum)]
2072 pub provider: DnsProviderKind,
2073 #[arg(long)]
2074 pub zone_id: String,
2075 #[arg(long)]
2076 pub token: Option<String>,
2077}
2078
2079#[derive(Args, Debug)]
2080pub struct DnsSettingsEditCmd {
2081 #[arg(long, value_enum)]
2082 pub provider: DnsProviderKind,
2083 #[arg(long)]
2084 pub zone_id: String,
2085 #[arg(long)]
2086 pub flatten_all_cnames: Option<bool>,
2087 #[arg(long)]
2088 pub foundation_dns: Option<bool>,
2089 #[arg(long)]
2090 pub multi_provider: Option<bool>,
2091 #[arg(long)]
2092 pub ns_ttl: Option<u32>,
2093 #[arg(long)]
2094 pub secondary_overrides: Option<bool>,
2095 #[arg(long)]
2096 pub zone_mode: Option<String>,
2097 #[arg(long = "reference-zone-id")]
2098 pub reference_zone_id: Option<String>,
2099 #[arg(long = "nameservers-type")]
2100 pub nameservers_type: Option<String>,
2101 #[arg(long = "nameservers-ns-set")]
2102 pub nameservers_ns_set: Option<u32>,
2103 #[arg(long = "soa-json")]
2104 pub soa_json: Option<String>,
2105 #[arg(long)]
2106 pub token: Option<String>,
2107}
2108
2109#[derive(Args, Debug)]
2110pub struct DomainsCmd {
2111 #[arg(long, value_enum)]
2112 pub provider: DomainsProviderKind,
2113 #[arg(long)]
2114 pub account_id: Option<String>,
2115 #[arg(long)]
2116 pub token: Option<String>,
2117 #[command(subcommand)]
2118 pub command: DomainsSubCommand,
2119}
2120
2121#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2122pub enum DomainsProviderKind {
2123 Cloudflare,
2124}
2125
2126#[derive(Subcommand, Debug)]
2127pub enum DomainsSubCommand {
2128 Search(DomainsSearchCmd),
2129 Check(DomainsCheckCmd),
2130 List(DomainsListCmd),
2131}
2132
2133#[derive(Args, Debug)]
2134pub struct DomainsSearchCmd {
2135 #[arg(long)]
2136 pub query: String,
2137 #[arg(long = "extension")]
2138 pub extensions: Vec<String>,
2139 #[arg(long)]
2140 pub limit: Option<usize>,
2141}
2142
2143#[derive(Args, Debug)]
2144pub struct DomainsCheckCmd {
2145 #[arg(long = "domain", required = true)]
2146 pub domains: Vec<String>,
2147}
2148
2149#[derive(Args, Debug)]
2150pub struct DomainsListCmd {}
2151
2152#[derive(Args, Debug)]
2153pub struct GenerateSystemdCmd {
2154 #[arg(
2155 long,
2156 default_value = "/etc/systemd/system",
2157 help = "Directory where the systemd units are written"
2158 )]
2159 pub output_dir: PathBuf,
2160 #[arg(long, help = "Only generate the unit for this service name")]
2161 pub service: Option<String>,
2162 #[arg(
2163 long,
2164 default_value_t = true,
2165 help = "Also generate the xbp-api systemd unit alongside project/services"
2166 )]
2167 pub api: bool,
2168}
2169
2170#[derive(Args, Debug)]
2171pub struct DoneCmd {
2172 #[arg(long, help = "Root directory under which to discover git repos")]
2173 pub root: Option<std::path::PathBuf>,
2174 #[arg(
2175 long,
2176 default_value = "24 hours ago",
2177 help = "Git --since value (e.g. '7 days ago')"
2178 )]
2179 pub since: String,
2180 #[arg(short, long, help = "Output Markdown file path")]
2181 pub output: Option<std::path::PathBuf>,
2182 #[arg(long, help = "Skip AI summarization (OpenRouter)")]
2183 pub no_ai: bool,
2184 #[arg(short, long, help = "Discover repos recursively")]
2185 pub recursive: bool,
2186 #[arg(long, help = "Exclude repo by name (repeatable)")]
2187 pub exclude: Vec<String>,
2188}
2189
2190#[cfg(feature = "nordvpn")]
2191#[derive(Args, Debug)]
2192pub struct NordvpnCmd {
2193 #[arg(
2194 trailing_var_arg = true,
2195 allow_hyphen_values = true,
2196 help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
2197 )]
2198 pub args: Vec<String>,
2199}
2200
2201#[cfg(feature = "kubernetes")]
2202#[derive(Args, Debug)]
2203pub struct KubernetesCmd {
2204 #[command(subcommand)]
2205 pub command: KubernetesSubCommand,
2206}
2207
2208#[cfg(feature = "kubernetes")]
2209#[derive(Args, Debug)]
2210pub struct KubernetesAddonCmd {
2211 #[command(subcommand)]
2212 pub command: KubernetesAddonSubCommand,
2213}
2214
2215#[cfg(feature = "kubernetes")]
2216#[derive(Subcommand, Debug)]
2217pub enum KubernetesAddonSubCommand {
2218 List,
2220 Enable {
2222 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
2223 name: String,
2224 },
2225 Disable {
2227 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
2228 name: String,
2229 },
2230}
2231
2232#[cfg(feature = "kubernetes")]
2233#[derive(Subcommand, Debug)]
2234pub enum KubernetesSubCommand {
2235 Check {
2237 #[arg(long, help = "Kubeconfig context to target")]
2238 context: Option<String>,
2239 #[arg(
2240 long,
2241 default_value = "default",
2242 help = "Namespace to probe for workload readiness"
2243 )]
2244 namespace: String,
2245 #[arg(long, help = "Skip live cluster calls (tooling check only)")]
2246 offline: bool,
2247 },
2248 Generate {
2250 #[arg(long, help = "Logical app name (used for resource names)")]
2251 name: String,
2252 #[arg(long, help = "Container image reference")]
2253 image: String,
2254 #[arg(long, default_value_t = 80, help = "Container port for the service")]
2255 port: u16,
2256 #[arg(long, default_value_t = 1, help = "Replica count")]
2257 replicas: u16,
2258 #[arg(
2259 long,
2260 default_value = "default",
2261 help = "Namespace for generated resources"
2262 )]
2263 namespace: String,
2264 #[arg(
2265 long,
2266 default_value = "k8s/xbp-manifest.yaml",
2267 help = "Path to write the manifest bundle"
2268 )]
2269 output: String,
2270 #[arg(long, help = "Optional ingress host (creates Ingress when set)")]
2271 host: Option<String>,
2272 },
2273 Apply {
2275 #[arg(long, help = "Path to manifest file")]
2276 file: String,
2277 #[arg(long, help = "Override kube context")]
2278 context: Option<String>,
2279 #[arg(long, help = "Override namespace")]
2280 namespace: Option<String>,
2281 #[arg(long, help = "Use --dry-run=server")]
2282 dry_run: bool,
2283 },
2284 Status {
2286 #[arg(long, default_value = "default", help = "Namespace to summarize")]
2287 namespace: String,
2288 #[arg(long, help = "Override kube context")]
2289 context: Option<String>,
2290 },
2291 Addons(KubernetesAddonCmd),
2293 DashboardToken {
2295 #[arg(
2296 long,
2297 default_value = "kube-system",
2298 help = "Namespace containing the dashboard token secret"
2299 )]
2300 namespace: String,
2301 #[arg(
2302 long,
2303 default_value = "microk8s-dashboard-token",
2304 help = "Secret name containing the dashboard login token"
2305 )]
2306 secret: String,
2307 #[arg(long, help = "Override kube context")]
2308 context: Option<String>,
2309 },
2310 ObservabilityCreds {
2312 #[arg(
2313 long,
2314 default_value = "observability",
2315 help = "Namespace containing Grafana secret"
2316 )]
2317 namespace: String,
2318 #[arg(
2319 long,
2320 default_value = "kube-prom-stack-grafana",
2321 help = "Grafana secret name"
2322 )]
2323 secret: String,
2324 #[arg(long, help = "Override kube context")]
2325 context: Option<String>,
2326 },
2327 Issuer {
2329 #[arg(
2330 long,
2331 help = "Email used for Let's Encrypt account registration (required)"
2332 )]
2333 email: String,
2334 #[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
2335 name: String,
2336 #[arg(
2337 long,
2338 default_value = "default",
2339 help = "Namespace for the Issuer resource"
2340 )]
2341 namespace: String,
2342 #[arg(
2343 long,
2344 default_value = "https://acme-v02.api.letsencrypt.org/directory",
2345 help = "ACME server URL (production by default)"
2346 )]
2347 server: String,
2348 #[arg(
2349 long,
2350 default_value = "letsencrypt-account-key",
2351 help = "Secret used to store the ACME account private key"
2352 )]
2353 private_key_secret: String,
2354 #[arg(
2355 long,
2356 default_value = "nginx",
2357 help = "Ingress class name used for HTTP01 solving"
2358 )]
2359 ingress_class_name: String,
2360 #[arg(long, help = "Override kube context")]
2361 context: Option<String>,
2362 #[arg(long, help = "Use --dry-run=server")]
2363 dry_run: bool,
2364 },
2365}
2366
2367#[cfg(test)]
2368mod tests {
2369 use super::{
2370 Cli, CloudflareConfigAction, CloudflaredSubCommand, Commands, DnsProviderKind,
2371 DnsSubCommand, DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand,
2372 GenerateSubCommand, LinearConfigAction, NetworkFloatingIpSubCommand,
2373 NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand, NetworkSubCommand, SshCmd,
2374 };
2375 #[cfg(feature = "secrets")]
2376 use super::{
2377 CloudflareSecretsSubCommand, SecretsEnvironment, SecretsProviderKind,
2378 SecretsStoresSubCommand, SecretsSubCommand,
2379 };
2380 use clap::Parser;
2381 use std::path::PathBuf;
2382
2383 #[test]
2384 fn parses_network_floating_ip_add() {
2385 let cli = Cli::parse_from([
2386 "xbp",
2387 "network",
2388 "floating-ip",
2389 "add",
2390 "--ip",
2391 "1.2.3.4",
2392 "--apply",
2393 ]);
2394
2395 match cli.command {
2396 Some(Commands::Network(network)) => match network.command {
2397 NetworkSubCommand::FloatingIp(fip) => match fip.command {
2398 NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
2399 assert_eq!(ip, "1.2.3.4");
2400 assert!(apply);
2401 }
2402 _ => panic!("expected add subcommand"),
2403 },
2404 _ => panic!("expected floating-ip subcommand"),
2405 },
2406 _ => panic!("expected network command"),
2407 }
2408 }
2409
2410 #[test]
2411 fn parses_generate_config_update() {
2412 let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
2413
2414 match cli.command {
2415 Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
2416 GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
2417 _ => panic!("expected generate config command"),
2418 },
2419 _ => panic!("expected generate command"),
2420 }
2421 }
2422
2423 #[test]
2424 fn parses_commit_command_with_dry_run() {
2425 let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
2426
2427 match cli.command {
2428 Some(Commands::Commit(commit_cmd)) => {
2429 assert!(commit_cmd.dry_run);
2430 assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
2431 assert_eq!(commit_cmd.model, "openai/gpt-4o-mini");
2432 }
2433 _ => panic!("expected commit command"),
2434 }
2435 }
2436
2437 #[test]
2438 fn parses_linear_select_initiative_config_command() {
2439 let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
2440
2441 match cli.command {
2442 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
2443 Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
2444 assert!(matches!(
2445 linear_cmd.action,
2446 LinearConfigAction::SelectInitiative
2447 ));
2448 }
2449 _ => panic!("expected linear config provider"),
2450 },
2451 _ => panic!("expected config command"),
2452 }
2453 }
2454
2455 #[test]
2456 fn parses_ssh_command_with_cloudflared_and_key_auth() {
2457 let cli = Cli::parse_from([
2458 "xbp",
2459 "ssh",
2460 "--host",
2461 "ssh.internal",
2462 "--username",
2463 "deploy",
2464 "--private-key",
2465 "C:/Users/floris/.ssh/id_ed25519",
2466 "--cloudflared-hostname",
2467 "bastion.example.com",
2468 "--command",
2469 "htop",
2470 ]);
2471
2472 let Some(Commands::Ssh(SshCmd {
2473 ssh_host,
2474 ssh_username,
2475 private_key,
2476 cloudflared_hostname,
2477 command,
2478 ..
2479 })) = cli.command
2480 else {
2481 panic!("expected shell command");
2482 };
2483
2484 assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
2485 assert_eq!(ssh_username.as_deref(), Some("deploy"));
2486 assert_eq!(
2487 private_key,
2488 Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
2489 );
2490 assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
2491 assert_eq!(command.as_deref(), Some("htop"));
2492 }
2493
2494 #[test]
2495 fn parses_cloudflared_tcp_command() {
2496 let cli = Cli::parse_from([
2497 "xbp",
2498 "cloudflared",
2499 "tcp",
2500 "--hostname",
2501 "bastion.example.com",
2502 "--listener",
2503 "127.0.0.1:2222",
2504 ]);
2505
2506 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
2507 panic!("expected cloudflared command");
2508 };
2509
2510 match cloudflared_cmd.command {
2511 CloudflaredSubCommand::Tcp(tcp_cmd) => {
2512 assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
2513 assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
2514 }
2515 }
2516 }
2517
2518 #[test]
2519 fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
2520 let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
2521
2522 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
2523 panic!("expected cloudflared command");
2524 };
2525
2526 match cloudflared_cmd.command {
2527 CloudflaredSubCommand::Tcp(tcp_cmd) => {
2528 assert_eq!(tcp_cmd.hostname, None);
2529 assert_eq!(tcp_cmd.listener, None);
2530 }
2531 }
2532 }
2533
2534 #[test]
2535 fn parses_version_workspace_publish_run_command() {
2536 let cli = Cli::parse_from([
2537 "xbp",
2538 "version",
2539 "workspace",
2540 "publish",
2541 "run",
2542 "--repo",
2543 "C:/Users/floris/Documents/GitHub/athena",
2544 "--dry-run",
2545 "--from",
2546 "athena-s3",
2547 ]);
2548
2549 let Some(Commands::Version(version_cmd)) = cli.command else {
2550 panic!("expected version command");
2551 };
2552
2553 match version_cmd.command {
2554 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
2555 match workspace_cmd.command {
2556 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
2557 match publish_cmd.command {
2558 super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
2559 assert_eq!(
2560 run_cmd.target.repo,
2561 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
2562 );
2563 assert!(!run_cmd.target.json);
2564 assert!(run_cmd.dry_run);
2565 assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
2566 }
2567 _ => panic!("expected publish run"),
2568 }
2569 }
2570 _ => panic!("expected workspace publish"),
2571 }
2572 }
2573 _ => panic!("expected version workspace command"),
2574 }
2575 }
2576
2577 #[test]
2578 fn parses_commit_alias_with_push_flag() {
2579 let cli = Cli::parse_from(["xbp", "c", "-p"]);
2580
2581 let Some(Commands::Commit(commit_cmd)) = cli.command else {
2582 panic!("expected commit command");
2583 };
2584
2585 assert!(commit_cmd.push);
2586 assert!(!commit_cmd.dry_run);
2587 }
2588
2589 #[test]
2590 fn parses_version_alias_release_alias() {
2591 let cli = Cli::parse_from(["xbp", "v", "r", "--draft"]);
2592
2593 let Some(Commands::Version(version_cmd)) = cli.command else {
2594 panic!("expected version command");
2595 };
2596
2597 let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
2598 panic!("expected release subcommand");
2599 };
2600
2601 assert!(release_cmd.draft);
2602 }
2603
2604 #[test]
2605 fn parses_publish_command_target_filter() {
2606 let cli = Cli::parse_from(["xbp", "publish", "--allow-dirty", "--target", "npm"]);
2607
2608 let Some(Commands::Publish(publish_cmd)) = cli.command else {
2609 panic!("expected publish command");
2610 };
2611
2612 assert!(publish_cmd.allow_dirty);
2613 assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
2614 }
2615
2616 #[test]
2617 fn parses_npm_setup_release_config_command() {
2618 let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
2619
2620 let Some(Commands::Config(config_cmd)) = cli.command else {
2621 panic!("expected config command");
2622 };
2623 let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
2624 panic!("expected npm config command");
2625 };
2626
2627 assert!(matches!(
2628 registry_cmd.action,
2629 super::RegistryConfigAction::SetupRelease
2630 ));
2631 }
2632
2633 #[test]
2634 fn parses_shell_alias_as_ssh_command() {
2635 let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
2636
2637 let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
2638 panic!("expected ssh command through shell alias");
2639 };
2640
2641 assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
2642 }
2643
2644 #[test]
2645 fn parses_api_request_command() {
2646 let cli = Cli::parse_from([
2647 "xbp",
2648 "api",
2649 "request",
2650 "/api/registry/installers/python-pip",
2651 "--web",
2652 "--method",
2653 "GET",
2654 "--header",
2655 "accept: application/json",
2656 ]);
2657
2658 let Some(Commands::Api(api_cmd)) = cli.command else {
2659 panic!("expected api command");
2660 };
2661
2662 match api_cmd.command {
2663 super::ApiSubCommand::Request(request_cmd) => {
2664 assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
2665 assert!(request_cmd.target.web);
2666 assert_eq!(request_cmd.method.as_deref(), Some("GET"));
2667 assert_eq!(
2668 request_cmd.target.header,
2669 vec!["accept: application/json".to_string()]
2670 );
2671 }
2672 _ => panic!("expected api request subcommand"),
2673 }
2674 }
2675
2676 #[test]
2677 fn parses_api_projects_list_command() {
2678 let cli = Cli::parse_from([
2679 "xbp",
2680 "api",
2681 "projects",
2682 "list",
2683 "--organization-id",
2684 "org_123",
2685 ]);
2686
2687 let Some(Commands::Api(api_cmd)) = cli.command else {
2688 panic!("expected api command");
2689 };
2690
2691 match api_cmd.command {
2692 super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
2693 super::ApiProjectsSubCommand::List(list_cmd) => {
2694 assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
2695 }
2696 _ => panic!("expected projects list subcommand"),
2697 },
2698 _ => panic!("expected projects subcommand"),
2699 }
2700 }
2701
2702 #[test]
2703 fn parses_api_routes_create_command() {
2704 let cli = Cli::parse_from([
2705 "xbp",
2706 "api",
2707 "routes",
2708 "create",
2709 "--domain",
2710 "demo.local",
2711 "--target",
2712 "http://127.0.0.1:3000",
2713 "--weighted-target",
2714 "http://127.0.0.1:3001=3",
2715 "--base-url",
2716 "http://127.0.0.1:8080",
2717 ]);
2718
2719 let Some(Commands::Api(api_cmd)) = cli.command else {
2720 panic!("expected api command");
2721 };
2722
2723 match api_cmd.command {
2724 super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
2725 super::ApiRoutesSubCommand::Create(create_cmd) => {
2726 assert_eq!(create_cmd.domain, "demo.local");
2727 assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
2728 assert_eq!(
2729 create_cmd.weighted_target,
2730 vec!["http://127.0.0.1:3001=3".to_string()]
2731 );
2732 assert_eq!(
2733 create_cmd.target_options.base_url.as_deref(),
2734 Some("http://127.0.0.1:8080")
2735 );
2736 }
2737 _ => panic!("expected routes create subcommand"),
2738 },
2739 _ => panic!("expected routes subcommand"),
2740 }
2741 }
2742
2743 #[test]
2744 fn parses_hetzner_vswitch_setup_command() {
2745 let cli = Cli::parse_from([
2746 "xbp",
2747 "network",
2748 "hetzner",
2749 "vswitch",
2750 "setup",
2751 "--ip",
2752 "10.0.3.2",
2753 "--vlan-id",
2754 "4000",
2755 "--interface",
2756 "enp0s31f6",
2757 "--apply",
2758 ]);
2759
2760 let Some(Commands::Network(network_cmd)) = cli.command else {
2761 panic!("expected network command");
2762 };
2763
2764 match network_cmd.command {
2765 NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
2766 NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
2767 NetworkHetznerVswitchSubCommand::Setup {
2768 ip,
2769 cidr,
2770 interface,
2771 vlan_id,
2772 apply,
2773 ..
2774 } => {
2775 assert_eq!(ip, "10.0.3.2");
2776 assert_eq!(cidr, 24);
2777 assert_eq!(interface.as_deref(), Some("enp0s31f6"));
2778 assert_eq!(vlan_id, 4000);
2779 assert!(apply);
2780 }
2781 },
2782 },
2783 _ => panic!("expected hetzner subcommand"),
2784 }
2785 }
2786
2787 #[cfg(feature = "secrets")]
2788 #[test]
2789 fn parses_secrets_diag_command() {
2790 let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
2791
2792 match cli.command {
2793 Some(Commands::Secrets(secrets_cmd)) => {
2794 assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
2795 assert!(matches!(
2796 secrets_cmd.environment,
2797 SecretsEnvironment::XbpDev
2798 ));
2799 }
2800 _ => panic!("expected secrets command"),
2801 }
2802 }
2803
2804 #[cfg(feature = "secrets")]
2805 #[test]
2806 fn parses_secrets_environment_override() {
2807 let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
2808
2809 match cli.command {
2810 Some(Commands::Secrets(secrets_cmd)) => {
2811 assert!(matches!(
2812 secrets_cmd.environment,
2813 SecretsEnvironment::XbpProd
2814 ));
2815 assert!(matches!(
2816 secrets_cmd.command,
2817 Some(SecretsSubCommand::Push(_))
2818 ));
2819 }
2820 _ => panic!("expected secrets command"),
2821 }
2822 }
2823
2824 #[cfg(feature = "secrets")]
2825 #[test]
2826 fn parses_secrets_providers_command() {
2827 let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
2828
2829 match cli.command {
2830 Some(Commands::Secrets(secrets_cmd)) => {
2831 assert!(matches!(
2832 secrets_cmd.command,
2833 Some(SecretsSubCommand::Providers)
2834 ));
2835 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
2836 }
2837 _ => panic!("expected secrets command"),
2838 }
2839 }
2840
2841 #[cfg(feature = "secrets")]
2842 #[test]
2843 fn parses_cloudflare_secret_store_create() {
2844 let cli = Cli::parse_from([
2845 "xbp",
2846 "secrets",
2847 "--provider",
2848 "cloudflare",
2849 "stores",
2850 "create",
2851 "--name",
2852 "prod",
2853 ]);
2854
2855 match cli.command {
2856 Some(Commands::Secrets(secrets_cmd)) => {
2857 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
2858 match secrets_cmd.command {
2859 Some(SecretsSubCommand::Stores(stores_cmd)) => {
2860 assert!(matches!(
2861 stores_cmd.command,
2862 SecretsStoresSubCommand::Create(_)
2863 ));
2864 }
2865 _ => panic!("expected stores subcommand"),
2866 }
2867 }
2868 _ => panic!("expected secrets command"),
2869 }
2870 }
2871
2872 #[cfg(feature = "secrets")]
2873 #[test]
2874 fn parses_cloudflare_secret_duplicate() {
2875 let cli = Cli::parse_from([
2876 "xbp",
2877 "secrets",
2878 "--provider",
2879 "cloudflare",
2880 "secrets",
2881 "duplicate",
2882 "--store-id",
2883 "store_1",
2884 "--secret-id",
2885 "secret_1",
2886 "--name",
2887 "COPY",
2888 ]);
2889
2890 match cli.command {
2891 Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
2892 Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
2893 assert!(matches!(
2894 secrets_cmd.command,
2895 CloudflareSecretsSubCommand::Duplicate(_)
2896 ));
2897 }
2898 _ => panic!("expected cloudflare secrets subcommand"),
2899 },
2900 _ => panic!("expected secrets command"),
2901 }
2902 }
2903
2904 #[test]
2905 fn parses_dns_providers_command() {
2906 let cli = Cli::parse_from(["xbp", "dns", "providers"]);
2907
2908 match cli.command {
2909 Some(Commands::Dns(dns_cmd)) => {
2910 assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
2911 }
2912 _ => panic!("expected dns command"),
2913 }
2914 }
2915
2916 #[test]
2917 fn parses_dns_zone_list_command() {
2918 let cli = Cli::parse_from([
2919 "xbp",
2920 "dns",
2921 "zones",
2922 "list",
2923 "--provider",
2924 "cloudflare",
2925 "--account-name-op",
2926 "contains",
2927 "--type",
2928 "full,partial",
2929 ]);
2930
2931 match cli.command {
2932 Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
2933 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
2934 DnsZonesSubCommand::List(list_cmd) => {
2935 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
2936 assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
2937 assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
2938 }
2939 _ => panic!("expected dns zones list"),
2940 },
2941 _ => panic!("expected dns zones"),
2942 },
2943 _ => panic!("expected dns command"),
2944 }
2945 }
2946
2947 #[test]
2948 fn parses_domains_search_command() {
2949 let cli = Cli::parse_from([
2950 "xbp",
2951 "domains",
2952 "--provider",
2953 "cloudflare",
2954 "search",
2955 "--query",
2956 "xbp",
2957 "--extension",
2958 "com",
2959 ]);
2960
2961 match cli.command {
2962 Some(Commands::Domains(domains_cmd)) => {
2963 assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
2964 assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
2965 }
2966 _ => panic!("expected domains command"),
2967 }
2968 }
2969
2970 #[test]
2971 fn parses_cloudflare_config_account_id_command() {
2972 let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
2973
2974 match cli.command {
2975 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
2976 Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
2977 assert!(matches!(
2978 cloudflare_cmd.action,
2979 CloudflareConfigAction::SetAccountId { .. }
2980 ));
2981 }
2982 _ => panic!("expected cloudflare config provider"),
2983 },
2984 _ => panic!("expected config command"),
2985 }
2986 }
2987}