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