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 = crate::cli::help_render::XBP_ROOT_HELP_TEMPLATE,
13 after_help = crate::cli::help_render::XBP_ROOT_AFTER_HELP
14)]
15pub struct Cli {
16 #[arg(long, global = true, help = "Enable verbose debugging output")]
17 pub debug: bool,
18 #[arg(short = 'l', help = "List pm2 processes")]
19 pub list: bool,
20 #[arg(short = 'p', long = "port", help = "Filter by port number")]
21 pub port: Option<u16>,
22 #[arg(long, help = "Open logs directory")]
23 pub logs: bool,
24 #[arg(long, help = "Print the complete alphabetical command reference")]
25 pub commands: bool,
26
27 #[command(subcommand)]
28 pub command: Option<Commands>,
29}
30
31#[derive(Subcommand, Debug)]
32pub enum Commands {
33 #[command(about = "Inspect or manage listening ports")]
34 Ports(PortsCmd),
35 #[command(
36 about = "Analyze the current git worktree and create a conventional commit",
37 visible_alias = "c"
38 )]
39 Commit(CommitCmd),
40 #[command(about = "Initialize an XBP project in the current directory")]
41 Init,
42 #[command(about = "Install common dependencies for host setup")]
43 Setup,
44 #[command(about = "Redeploy one service or the entire project")]
45 Redeploy {
46 #[arg(
47 help = "Service name to redeploy (optional, uses legacy redeploy.sh if not provided)"
48 )]
49 service_name: Option<String>,
50 },
51 #[command(about = "Run the legacy remote redeploy workflow over SSH")]
52 RedeployV2(RedeployV2Cmd),
53 #[command(about = "Inspect project/global config and manage provider keys")]
54 Config(ConfigCmd),
55 #[command(
56 about = "Install supported host packages or project tooling",
57 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
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 or inspect the CLI login flow against the XBP dashboard")]
111 Login(LoginCmd),
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(
137 about = "Manage Cloudflare Workers secrets, Wrangler config helpers, D1 migrations, and deploy flows",
138 visible_alias = "worker"
139 )]
140 Workers(WorkersCmd),
141 #[command(about = "Manage DNS providers, zones, records, DNSSEC, and settings")]
142 Dns(DnsCmd),
143 #[command(about = "Discover and inspect registered domains")]
144 Domains(DomainsCmd),
145 #[command(
146 about = "Generate 'what did I get done' Markdown report from git commits across repos"
147 )]
148 Done(DoneCmd),
149 #[command(
150 about = "Repair malformed Cursor process-monitor JSON exports",
151 visible_alias = "fix-pm-json"
152 )]
153 FixProcessMonitorJson(FixProcessMonitorJsonCmd),
154 #[command(about = "Upload Cursor local file history to the XBP dashboard")]
155 Cursor(CursorCmd),
156 #[cfg(feature = "kubernetes")]
157 #[command(about = "Experimental Kubernetes cluster manager (feature-gated)")]
158 Kubernetes(KubernetesCmd),
159 #[cfg(feature = "nordvpn")]
160 #[command(about = "NordVPN meshnet setup and passthrough (feature-gated)")]
161 Nordvpn(NordvpnCmd),
162 #[cfg(feature = "monitoring")]
163 Monitoring(MonitoringCmd),
164 #[command(about = "Manage the XBP API server")]
165 Api(ApiCmd),
166 #[command(about = "Built-in MCP server for AI agents (HTTP/SSE on port 1113)")]
167 Mcp(McpCmd),
168 #[cfg(feature = "docker")]
169 #[command(about = "Pass-through wrapper around the Docker CLI")]
170 Docker(DockerCmd),
171}
172
173pub fn command_label(command: &Commands) -> &'static str {
174 match command {
175 Commands::Ports(_) => "ports",
176 Commands::Commit(_) => "commit",
177 Commands::Init => "init",
178 Commands::Setup => "setup",
179 Commands::Redeploy { .. } => "redeploy",
180 Commands::RedeployV2(_) => "redeploy-v2",
181 Commands::Config(_) => "config",
182 Commands::Install { .. } => "install",
183 Commands::Logs(_) => "logs",
184 Commands::Ssh(_) => "ssh",
185 Commands::Cloudflared(_) => "cloudflared",
186 Commands::List => "list",
187 Commands::Curl(_) => "curl",
188 Commands::Services => "services",
189 Commands::Service { .. } => "service",
190 Commands::Nginx(_) => "nginx",
191 Commands::Network(_) => "network",
192 Commands::Diag(_) => "diag",
193 Commands::Monitor(_) => "monitor",
194 Commands::Snapshot => "snapshot",
195 Commands::Resurrect => "resurrect",
196 Commands::Stop { .. } => "stop",
197 Commands::Flush { .. } => "flush",
198 Commands::Login(_) => "login",
199 Commands::Version(_) => "version",
200 Commands::Publish(_) => "publish",
201 Commands::Env { .. } => "env",
202 Commands::Tail(_) => "tail",
203 Commands::Start { .. } => "start",
204 Commands::Generate(_) => "generate",
205 #[cfg(feature = "secrets")]
206 Commands::Secrets(_) => "secrets",
207 Commands::Workers(_) => "workers",
208 Commands::Dns(_) => "dns",
209 Commands::Domains(_) => "domains",
210 Commands::Done(_) => "done",
211 Commands::FixProcessMonitorJson(_) => "fix-process-monitor-json",
212 Commands::Cursor(_) => "cursor",
213 #[cfg(feature = "kubernetes")]
214 Commands::Kubernetes(_) => "kubernetes",
215 #[cfg(feature = "nordvpn")]
216 Commands::Nordvpn(_) => "nordvpn",
217 #[cfg(feature = "monitoring")]
218 Commands::Monitoring(_) => "monitoring",
219 Commands::Api(_) => "api",
220 Commands::Mcp(_) => "mcp",
221 #[cfg(feature = "docker")]
222 Commands::Docker(_) => "docker",
223 }
224}
225
226#[derive(Args, Debug)]
227#[command(
228 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
229 after_help = crate::cli::help_render::COMMIT_AFTER_HELP
230)]
231pub struct CommitCmd {
232 #[arg(
233 long,
234 help = "Generate and print the conventional commit message without creating a git commit"
235 )]
236 pub dry_run: bool,
237 #[arg(
238 short = 'p',
239 long,
240 help = "Push after committing, or push pending local commits when nothing new needs committing"
241 )]
242 pub push: bool,
243 #[arg(long, help = "Skip OpenRouter and use local heuristics only")]
244 pub no_ai: bool,
245 #[arg(
246 long,
247 help = "OpenRouter model override used for commit generation; otherwise XBP uses the global config default"
248 )]
249 pub model: Option<String>,
250 #[arg(
251 long,
252 help = "Force the conventional commit scope (for example: cli, api, docs)"
253 )]
254 pub scope: Option<String>,
255}
256
257#[derive(Args, Debug)]
258#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
259pub struct PortsCmd {
260 #[arg(short = 'p', long = "port")]
261 pub port: Option<u16>,
262 #[arg(long = "kill")]
263 pub kill: bool,
264 #[arg(short = 'n', long = "nginx")]
265 pub nginx: bool,
266 #[arg(
267 long = "full",
268 help = "Show one unified ports view (reconciled listeners + exposure + security flags)"
269 )]
270 pub full: bool,
271 #[arg(
272 long = "no-local",
273 help = "Exclude connections where LocalAddr equals RemoteAddr"
274 )]
275 pub no_local: bool,
276 #[arg(
277 long = "exposure",
278 help = "Diagnose external exposure per port (binding + firewall layer)"
279 )]
280 pub exposure: bool,
281}
282
283#[derive(Args, Debug)]
284#[command(
285 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
286 after_help = crate::cli::help_render::CONFIG_AFTER_HELP
287)]
288pub struct ConfigCmd {
289 #[arg(
290 long,
291 help = "Show the current project config instead of opening global XBP paths"
292 )]
293 pub project: bool,
294 #[arg(long, help = "Print global XBP paths without opening them")]
295 pub no_open: bool,
296 #[command(subcommand)]
297 pub provider: Option<ConfigProviderCmd>,
298}
299
300#[derive(Subcommand, Debug)]
301pub enum ConfigProviderCmd {
302 #[command(about = "Manage the OpenRouter API key used by AI-enabled commands")]
303 Openrouter(ConfigSecretCmd),
304 #[command(about = "Manage the GitHub OAuth2 token used for release automation")]
305 Github(ConfigSecretCmd),
306 #[command(
307 about = "Manage Cloudflare API credentials used by secrets, DNS, and domains (run without a subcommand for the interactive setup wizard)"
308 )]
309 Cloudflare(CloudflareConfigCmd),
310 #[command(
311 about = "Manage the Linear API key used for release-note issue linking and initiative publishing"
312 )]
313 Linear(LinearConfigCmd),
314 #[command(about = "Manage npm registry auth and guided npm publish config")]
315 Npm(RegistryConfigCmd),
316 #[command(about = "Manage crates.io auth and guided crate publish config")]
317 Crates(CratesConfigCmd),
318}
319
320#[derive(Args, Debug)]
321pub struct ConfigSecretCmd {
322 #[command(subcommand)]
323 pub action: ConfigSecretAction,
324}
325
326#[derive(Subcommand, Debug)]
327pub enum ConfigSecretAction {
328 #[command(about = "Set provider key (omit value to enter it securely)")]
329 SetKey {
330 #[arg(help = "Provider key/token value")]
331 key: Option<String>,
332 },
333 #[command(about = "Delete the stored provider key")]
334 DeleteKey,
335 #[command(about = "Show whether a key is configured (masked by default)")]
336 Show {
337 #[arg(long, help = "Print full key/token value (not masked)")]
338 raw: bool,
339 },
340}
341
342#[derive(Args, Debug)]
343pub struct CloudflareConfigCmd {
344 #[command(subcommand)]
345 pub action: Option<CloudflareConfigAction>,
346}
347
348#[derive(Subcommand, Debug)]
349pub enum CloudflareConfigAction {
350 #[command(about = "Set Cloudflare API token (omit value to enter it securely)")]
351 SetKey {
352 #[arg(help = "Cloudflare API token")]
353 key: Option<String>,
354 },
355 #[command(about = "Delete the stored Cloudflare API token")]
356 DeleteKey,
357 #[command(about = "Show whether a Cloudflare API token is configured")]
358 ShowKey {
359 #[arg(long, help = "Print full token value (not masked)")]
360 raw: bool,
361 },
362 #[command(about = "Set the default Cloudflare account ID")]
363 SetAccountId {
364 #[arg(help = "Cloudflare account ID")]
365 account_id: Option<String>,
366 },
367 #[command(about = "Delete the stored default Cloudflare account ID")]
368 DeleteAccountId,
369 #[command(about = "Show whether a Cloudflare account ID is configured")]
370 ShowAccountId {
371 #[arg(long, help = "Print full account ID value (not masked)")]
372 raw: bool,
373 },
374 #[command(about = "Interactive dashboard OAuth linking flow for Cloudflare credentials")]
375 Login,
376 #[command(about = "Show Cloudflare credential sources and readiness")]
377 Status,
378 #[command(about = "Run the interactive Cloudflare credential setup wizard")]
379 Setup,
380}
381
382#[derive(Args, Debug)]
383pub struct LinearConfigCmd {
384 #[command(subcommand)]
385 pub action: LinearConfigAction,
386}
387
388#[derive(Subcommand, Debug)]
389pub enum LinearConfigAction {
390 #[command(about = "Set Linear API key (omit value to enter it securely)")]
391 SetKey {
392 #[arg(help = "Linear API key/token value")]
393 key: Option<String>,
394 },
395 #[command(about = "Delete the stored Linear API key")]
396 DeleteKey,
397 #[command(about = "Show whether a Linear API key is configured (masked by default)")]
398 Show {
399 #[arg(long, help = "Print full key/token value (not masked)")]
400 raw: bool,
401 },
402 #[command(
403 name = "select-initiative",
404 about = "Pick a Linear initiative for the current repo and save it to .xbp/xbp.yaml"
405 )]
406 SelectInitiative,
407}
408
409#[derive(Args, Debug)]
410pub struct RegistryConfigCmd {
411 #[command(subcommand)]
412 pub action: RegistryConfigAction,
413}
414
415#[derive(Args, Debug)]
416pub struct CratesConfigCmd {
417 #[command(subcommand)]
418 pub action: CratesConfigAction,
419}
420
421#[derive(Subcommand, Debug)]
422pub enum RegistryConfigAction {
423 #[command(about = "Set registry token/key (omit value to enter it securely)")]
424 SetKey {
425 #[arg(help = "Registry token value")]
426 key: Option<String>,
427 },
428 #[command(about = "Delete the stored registry token")]
429 DeleteKey,
430 #[command(about = "Show whether a registry token is configured (masked by default)")]
431 Show {
432 #[arg(long, help = "Print full token value (not masked)")]
433 raw: bool,
434 },
435 #[command(
436 name = "setup-release",
437 about = "Interactively configure project publish settings in .xbp/xbp.yaml"
438 )]
439 SetupRelease,
440}
441
442#[derive(Subcommand, Debug)]
443pub enum CratesConfigAction {
444 #[command(about = "Set crates.io token (omit value to enter it securely)")]
445 SetKey {
446 #[arg(help = "crates.io token value")]
447 key: Option<String>,
448 },
449 #[command(about = "Delete the stored crates.io token from global XBP config")]
450 DeleteKey,
451 #[command(about = "Show whether a crates.io token is configured (masked by default)")]
452 Show {
453 #[arg(long, help = "Print full token value (not masked)")]
454 raw: bool,
455 },
456 #[command(
457 name = "setup-release",
458 about = "Interactively configure project publish settings in .xbp/xbp.yaml"
459 )]
460 SetupRelease,
461 #[command(
462 about = "Run `cargo login` using the stored crates token and sync Cargo's local credentials file"
463 )]
464 Login {
465 #[arg(help = "Optional crates.io token value to save before logging in")]
466 key: Option<String>,
467 },
468 #[command(
469 about = "Run `cargo logout` to remove Cargo's local crates.io credentials while keeping XBP's stored token"
470 )]
471 Logout,
472}
473
474#[derive(Args, Debug)]
475#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
476pub struct CurlCmd {
477 #[arg(help = "URL or domain to fetch, e.g. example.com or https://example.com/api")]
478 pub url: Option<String>,
479 #[arg(long, help = "Disable the default 15 second timeout")]
480 pub no_timeout: bool,
481}
482
483#[derive(Args, Debug)]
484#[command(
485 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
486 after_help = crate::cli::help_render::LOGIN_AFTER_HELP
487)]
488pub struct LoginCmd {
489 #[command(subcommand)]
490 pub action: Option<LoginSubCommand>,
491}
492
493#[derive(Subcommand, Debug)]
494pub enum LoginSubCommand {
495 #[command(about = "Show whether the current CLI session is still valid")]
496 Status,
497 #[command(about = "Revoke the current CLI token and clear local login state")]
498 Logout,
499}
500
501#[derive(Args, Debug)]
502#[command(
503 subcommand_precedence_over_arg = true,
504 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
505 after_help = crate::cli::help_render::VERSION_AFTER_HELP
506)]
507pub struct VersionCmd {
508 #[arg(
509 help = "Show versions, bump with major/minor/patch, or set an explicit version like 1.2.3"
510 )]
511 pub target: Option<String>,
512 #[arg(
513 short = 'v',
514 long = "version",
515 help = "Explicit version target; equivalent to the positional version value and overrides it when both are provided"
516 )]
517 pub explicit_version: Option<String>,
518 #[arg(long, help = "Show normalized git tags from `git tag --list`")]
519 pub git: bool,
520 #[command(subcommand)]
521 pub command: Option<VersionSubCommand>,
522}
523
524#[derive(Subcommand, Debug)]
525pub enum VersionSubCommand {
526 #[command(
527 about = "Create and push a git tag for this version, then create a GitHub release",
528 visible_alias = "r"
529 )]
530 Release(VersionReleaseCmd),
531 #[command(
532 about = "Manage Rust workspace release/version drift, sync, validation, and publish flow",
533 arg_required_else_help = true
534 )]
535 Workspace(VersionWorkspaceCmd),
536 #[command(name = "discover", alias = "register", alias = "register-services")]
538 Discover(VersionDiscoverServicesCmd),
539 #[command(
540 about = "Bump versions only for packages with uncommitted changes in the working tree"
541 )]
542 Bump(VersionBumpCmd),
543}
544
545#[derive(Args, Debug)]
546pub struct VersionBumpCmd {
547 #[arg(long, help = "Preview bump selections without writing version files")]
548 pub dry_run: bool,
549 #[arg(
550 long,
551 group = "bump_kind",
552 help = "Default bump kind for --all and interactive default selection"
553 )]
554 pub major: bool,
555 #[arg(
556 long,
557 group = "bump_kind",
558 help = "Default bump kind for --all and interactive default"
559 )]
560 pub minor: bool,
561 #[arg(
562 long,
563 group = "bump_kind",
564 help = "Default bump kind for --all and interactive default"
565 )]
566 pub patch: bool,
567 #[arg(
568 long,
569 help = "Bump every mutated package with the selected default kind without prompting"
570 )]
571 pub all: bool,
572}
573
574#[derive(Args, Debug)]
575pub struct VersionDiscoverServicesCmd {
576 #[arg(
577 long,
578 help = "Preview discovered nested XBP services without writing .xbp/xbp.yaml"
579 )]
580 pub dry_run: bool,
581 #[arg(
582 long,
583 help = "Discover nested services only; do not register them in the root config (opt out)"
584 )]
585 pub no_register: bool,
586}
587
588#[derive(Args, Debug)]
589pub struct VersionReleaseCmd {
590 #[arg(
591 long,
592 help = "Release this version instead of auto-detecting from tracked files"
593 )]
594 pub version: Option<String>,
595 #[arg(
596 long,
597 help = "Allow releasing with uncommitted changes in the working tree"
598 )]
599 pub allow_dirty: bool,
600 #[arg(long, help = "Release title (defaults to <version> - <repo>)")]
601 pub title: Option<String>,
602 #[arg(long, help = "Release notes body (Markdown)")]
603 pub notes: Option<String>,
604 #[arg(long, help = "Read release notes body from a file")]
605 pub notes_file: Option<PathBuf>,
606 #[arg(long, help = "Create as draft release")]
607 pub draft: bool,
608 #[arg(long, help = "Mark release as pre-release")]
609 pub prerelease: bool,
610 #[arg(
611 long,
612 help = "Run configured npm/crates publish workflows before creating the GitHub release"
613 )]
614 pub publish: bool,
615 #[arg(
616 long,
617 requires = "publish",
618 help = "Skip configured publish preflight commands when `--publish` is enabled and allow a dirty working tree"
619 )]
620 pub force: bool,
621 #[arg(
622 long,
623 value_enum,
624 default_value_t = VersionReleaseLatest::Legacy,
625 help = "Control GitHub latest flag: true, false, or legacy"
626 )]
627 pub make_latest: VersionReleaseLatest,
628}
629
630#[derive(Copy, Clone, Debug, ValueEnum)]
631pub enum VersionReleaseLatest {
632 True,
633 False,
634 Legacy,
635}
636
637#[derive(Args, Debug)]
638#[command(
639 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
640 after_help = crate::cli::help_render::PUBLISH_AFTER_HELP
641)]
642pub struct PublishCmd {
643 #[arg(
644 long,
645 help = "Validate and print what would publish without uploading packages"
646 )]
647 pub dry_run: bool,
648 #[arg(
649 long,
650 help = "Allow publish workflows to run with a dirty working tree"
651 )]
652 pub allow_dirty: bool,
653 #[arg(long, help = "Skip configured preflight commands and publish anyway")]
654 pub force: bool,
655 #[arg(
656 long,
657 help = "When publishing a crate workspace member, also publish missing internal prerequisites in dependency order"
658 )]
659 pub include_prereqs: bool,
660 #[arg(long, help = "Limit publishing to one target: npm or crates")]
661 pub target: Option<String>,
662 #[arg(
663 long,
664 help = "Limit publishing to the workflow whose manifest matches this path"
665 )]
666 pub manifest_path: Option<PathBuf>,
667}
668
669#[derive(Args, Debug)]
670#[command(
671 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
672 after_help = "Examples:\n xbp version workspace check --repo C:/Users/floris/Documents/GitHub/athena\n xbp version workspace sync --version 3.16.5\n xbp version workspace sync --version 3.16.5 --write\n xbp version workspace validate --cargo-check --package-dry-run\n xbp version workspace publish plan\n xbp version workspace publish plan --only athena-auth --include-prereqs\n xbp version workspace publish run --dry-run\n xbp version workspace publish run --from athena-s3\n xbp version workspace publish run --only athena-auth --include-prereqs"
673)]
674pub struct VersionWorkspaceCmd {
675 #[command(subcommand)]
676 pub command: VersionWorkspaceSubCommand,
677}
678
679#[derive(Args, Debug, Clone, Default)]
680pub struct VersionWorkspaceTargetArgs {
681 #[arg(
682 long,
683 help = "Workspace repo root to inspect (defaults to current project root)"
684 )]
685 pub repo: Option<PathBuf>,
686 #[arg(long, help = "Emit machine-readable JSON output")]
687 pub json: bool,
688}
689
690#[derive(Subcommand, Debug)]
691pub enum VersionWorkspaceSubCommand {
692 #[command(about = "Detect workspace release drift and exit non-zero when mismatches exist")]
693 Check(VersionWorkspaceCheckCmd),
694 #[command(about = "Preview or apply workspace-wide version alignment")]
695 Sync(VersionWorkspaceSyncCmd),
696 #[command(about = "Run structural and optional cargo validation for workspace publishability")]
697 Validate(VersionWorkspaceValidateCmd),
698 #[command(about = "Plan or execute crates.io publishing for workspace packages")]
699 Publish(VersionWorkspacePublishCmd),
700}
701
702#[derive(Args, Debug)]
703pub struct VersionWorkspaceCheckCmd {
704 #[command(flatten)]
705 pub target: VersionWorkspaceTargetArgs,
706 #[arg(
707 long,
708 help = "Expected release version (defaults to the root package version)"
709 )]
710 pub version: Option<String>,
711}
712
713#[derive(Args, Debug)]
714pub struct VersionWorkspaceSyncCmd {
715 #[command(flatten)]
716 pub target: VersionWorkspaceTargetArgs,
717 #[arg(
718 long,
719 help = "Target release version (defaults to the root package version)"
720 )]
721 pub version: Option<String>,
722 #[arg(
723 long,
724 help = "Write changes to disk instead of previewing the sync plan"
725 )]
726 pub write: bool,
727}
728
729#[derive(Args, Debug)]
730pub struct VersionWorkspaceValidateCmd {
731 #[command(flatten)]
732 pub target: VersionWorkspaceTargetArgs,
733 #[arg(long, help = "Limit cargo validation to a single package name")]
734 pub package: Option<String>,
735 #[arg(long, help = "Run `cargo check -q` as part of validation")]
736 pub cargo_check: bool,
737 #[arg(
738 long,
739 help = "Run `cargo publish --dry-run --locked` for publishable packages"
740 )]
741 pub package_dry_run: bool,
742}
743
744#[derive(Args, Debug)]
745#[command(arg_required_else_help = true)]
746pub struct VersionWorkspacePublishCmd {
747 #[command(subcommand)]
748 pub command: VersionWorkspacePublishSubCommand,
749}
750
751#[derive(Subcommand, Debug)]
752pub enum VersionWorkspacePublishSubCommand {
753 #[command(about = "Show publish order, crates.io visibility, and blockers without publishing")]
754 Plan(VersionWorkspacePublishPlanCmd),
755 #[command(about = "Publish workspace packages in dependency order")]
756 Run(VersionWorkspacePublishRunCmd),
757}
758
759#[derive(Args, Debug)]
760pub struct VersionWorkspacePublishPlanCmd {
761 #[command(flatten)]
762 pub target: VersionWorkspaceTargetArgs,
763 #[arg(long, help = "Limit the plan to one package")]
764 pub only: Option<String>,
765 #[arg(
766 long,
767 help = "When planning a single package, also include missing internal prerequisites"
768 )]
769 pub include_prereqs: bool,
770}
771
772#[derive(Args, Debug)]
773pub struct VersionWorkspacePublishRunCmd {
774 #[command(flatten)]
775 pub target: VersionWorkspaceTargetArgs,
776 #[arg(long, help = "Preview publish actions without calling cargo publish")]
777 pub dry_run: bool,
778 #[arg(
779 long,
780 help = "Start publishing from this package in the computed order"
781 )]
782 pub from: Option<String>,
783 #[arg(long, help = "Publish only this package")]
784 pub only: Option<String>,
785 #[arg(
786 long,
787 help = "When publishing one package, also publish missing internal prerequisites in dependency order"
788 )]
789 pub include_prereqs: bool,
790 #[arg(long, help = "Continue publishing remaining packages after a failure")]
791 pub continue_on_error: bool,
792 #[arg(long, help = "Allow publishing from a dirty worktree")]
793 pub allow_dirty: bool,
794 #[arg(
795 long,
796 default_value_t = 180.0,
797 help = "How long to wait for each published version to become visible on crates.io"
798 )]
799 pub timeout_seconds: f64,
800 #[arg(
801 long,
802 default_value_t = 5.0,
803 help = "How often to poll crates.io for the just-published version"
804 )]
805 pub poll_interval_seconds: f64,
806}
807
808#[derive(Args, Debug)]
809pub struct RedeployV2Cmd {
810 #[arg(short = 'p', long = "password")]
811 pub password: Option<String>,
812 #[arg(short = 'u', long = "username")]
813 pub username: Option<String>,
814 #[arg(short = 'h', long = "host")]
815 pub host: Option<String>,
816 #[arg(short = 'd', long = "project-dir")]
817 pub project_dir: Option<String>,
818}
819
820#[derive(Args, Debug)]
821#[command(
822 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
823 after_help = crate::cli::help_render::LOGS_AFTER_HELP
824)]
825pub struct LogsCmd {
826 #[arg()]
827 pub project: Option<String>,
828 #[arg(long = "ssh-host", help = "SSH host to stream logs from")]
829 pub ssh_host: Option<String>,
830 #[arg(long = "ssh-username", help = "SSH username for remote host")]
831 pub ssh_username: Option<String>,
832 #[arg(long = "ssh-password", help = "SSH password for remote host")]
833 pub ssh_password: Option<String>,
834}
835
836#[derive(Args, Debug)]
837#[command(
838 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
839 after_help = crate::cli::help_render::SSH_AFTER_HELP
840)]
841pub struct SshCmd {
842 #[arg(long = "host", alias = "ssh-host", help = "SSH host or IP address")]
843 pub ssh_host: Option<String>,
844 #[arg(
845 long = "port",
846 default_value_t = 22,
847 help = "SSH port for direct connections"
848 )]
849 pub ssh_port: u16,
850 #[arg(
851 long = "username",
852 alias = "ssh-username",
853 help = "SSH username for the remote host"
854 )]
855 pub ssh_username: Option<String>,
856 #[arg(
857 long = "password",
858 alias = "ssh-password",
859 help = "SSH password (omit to use stored config or a secure prompt)"
860 )]
861 pub ssh_password: Option<String>,
862 #[arg(
863 long,
864 help = "Path to a private key file to use instead of password auth"
865 )]
866 pub private_key: Option<PathBuf>,
867 #[arg(long, help = "Passphrase for --private-key when required")]
868 pub private_key_passphrase: Option<String>,
869 #[arg(
870 long,
871 help = "Run this remote command in a PTY instead of opening the default login shell"
872 )]
873 pub command: Option<String>,
874 #[arg(
875 long,
876 help = "TERM value sent to the server (default: TERM env var or xterm-256color)"
877 )]
878 pub term: Option<String>,
879 #[arg(long, help = "Disable SSH host key verification")]
880 pub no_host_key_check: bool,
881 #[arg(
882 long,
883 help = "Pin the SSH host key as a base64 blob when using tunnels or first-connect flows"
884 )]
885 pub host_key: Option<String>,
886 #[arg(
887 long,
888 help = "Path to a known_hosts file used for SSH host verification"
889 )]
890 pub known_hosts_file: Option<PathBuf>,
891 #[arg(
892 long,
893 help = "Cloudflare Access hostname used to open a local cloudflared TCP forwarder"
894 )]
895 pub cloudflared_hostname: Option<String>,
896 #[arg(long, help = "Override the cloudflared binary path")]
897 pub cloudflared_binary: Option<PathBuf>,
898 #[arg(
899 long,
900 help = "Optional destination host:port passed to cloudflared access tcp"
901 )]
902 pub cloudflared_destination: Option<String>,
903}
904
905#[derive(Args, Debug)]
906#[command(
907 arg_required_else_help = true,
908 disable_help_subcommand = true,
909 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
910)]
911pub struct CloudflaredCmd {
912 #[command(subcommand)]
913 pub command: CloudflaredSubCommand,
914}
915
916#[derive(Subcommand, Debug)]
917pub enum CloudflaredSubCommand {
918 #[command(about = "Start a local cloudflared Access TCP forwarder")]
919 Tcp(CloudflaredTcpCmd),
920}
921
922#[derive(Args, Debug)]
923#[command(
924 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
925 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"
926)]
927pub struct CloudflaredTcpCmd {
928 #[arg(long, help = "Protected Cloudflare Access hostname")]
929 pub hostname: Option<String>,
930 #[arg(
931 long,
932 help = "Local listener address for the forwarder (default: auto-allocate 127.0.0.1:<port>)"
933 )]
934 pub listener: Option<String>,
935 #[arg(
936 long,
937 help = "Optional destination host:port passed to cloudflared access tcp"
938 )]
939 pub destination: Option<String>,
940 #[arg(long, help = "Override the cloudflared binary path")]
941 pub binary: Option<PathBuf>,
942}
943
944#[derive(Args, Debug)]
945#[command(
946 arg_required_else_help = true,
947 disable_help_subcommand = true,
948 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
949 after_help = crate::cli::help_render::NGINX_AFTER_HELP
950)]
951pub struct NginxCmd {
952 #[command(subcommand)]
953 pub command: NginxSubCommand,
954}
955
956#[derive(Args, Debug)]
957#[command(
958 arg_required_else_help = true,
959 disable_help_subcommand = true,
960 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
961)]
962pub struct NetworkCmd {
963 #[command(subcommand)]
964 pub command: NetworkSubCommand,
965}
966
967#[derive(Subcommand, Debug)]
968pub enum NetworkSubCommand {
969 #[command(about = "Manage persistent floating IP configuration")]
970 FloatingIp(NetworkFloatingIpCmd),
971 #[command(about = "Inspect discovered network configuration sources")]
972 Config(NetworkConfigCmd),
973 #[command(about = "Manage Hetzner-specific Linux network configuration")]
974 Hetzner(NetworkHetznerCmd),
975}
976
977#[derive(Args, Debug)]
978pub struct NetworkFloatingIpCmd {
979 #[command(subcommand)]
980 pub command: NetworkFloatingIpSubCommand,
981}
982
983#[derive(Subcommand, Debug)]
984pub enum NetworkFloatingIpSubCommand {
985 #[command(about = "Add a persistent floating IP entry to detected network backend")]
986 Add {
987 #[arg(long, help = "Floating IP address (IPv4 or IPv6)")]
988 ip: String,
989 #[arg(long, help = "CIDR suffix (defaults: IPv4=32, IPv6=64)")]
990 cidr: Option<u8>,
991 #[arg(long, help = "Network interface override (auto-detected when omitted)")]
992 interface: Option<String>,
993 #[arg(long, help = "Optional label for backend metadata/file naming")]
994 label: Option<String>,
995 #[arg(long, help = "Apply network changes after writing config")]
996 apply: bool,
997 #[arg(long, help = "Preview computed changes without writing files")]
998 dry_run: bool,
999 },
1000 #[command(about = "List floating IPs from runtime and persisted network config")]
1001 List {
1002 #[arg(long, help = "Emit JSON output")]
1003 json: bool,
1004 },
1005}
1006
1007#[derive(Args, Debug)]
1008pub struct NetworkConfigCmd {
1009 #[command(subcommand)]
1010 pub command: NetworkConfigSubCommand,
1011}
1012
1013#[derive(Subcommand, Debug)]
1014pub enum NetworkConfigSubCommand {
1015 #[command(about = "List detected backend and configuration source files")]
1016 List {
1017 #[arg(long, help = "Emit JSON output")]
1018 json: bool,
1019 },
1020}
1021
1022#[derive(Args, Debug)]
1023pub struct NetworkHetznerCmd {
1024 #[command(subcommand)]
1025 pub command: NetworkHetznerSubCommand,
1026}
1027
1028#[derive(Subcommand, Debug)]
1029pub enum NetworkHetznerSubCommand {
1030 #[command(about = "Configure a Hetzner vSwitch VLAN interface persistently")]
1031 Vswitch(NetworkHetznerVswitchCmd),
1032}
1033
1034#[derive(Args, Debug)]
1035pub struct NetworkHetznerVswitchCmd {
1036 #[command(subcommand)]
1037 pub command: NetworkHetznerVswitchSubCommand,
1038}
1039
1040#[derive(Subcommand, Debug)]
1041pub enum NetworkHetznerVswitchSubCommand {
1042 #[command(about = "Write persistent Linux config for a Hetzner vSwitch VLAN interface")]
1043 Setup {
1044 #[arg(
1045 long,
1046 help = "Private IPv4 address to assign on the vSwitch VLAN interface"
1047 )]
1048 ip: String,
1049 #[arg(
1050 long,
1051 default_value_t = 24,
1052 help = "CIDR prefix for --ip (default: 24)"
1053 )]
1054 cidr: u8,
1055 #[arg(long, help = "Physical parent interface (auto-detected when omitted)")]
1056 interface: Option<String>,
1057 #[arg(long, help = "Hetzner vSwitch VLAN ID")]
1058 vlan_id: u16,
1059 #[arg(long, default_value_t = 1400, help = "Interface MTU (default: 1400)")]
1060 mtu: u16,
1061 #[arg(
1062 long,
1063 default_value = "10.0.3.1",
1064 help = "Gateway for the routed Hetzner cloud network"
1065 )]
1066 gateway: String,
1067 #[arg(
1068 long,
1069 default_value = "10.0.0.0/16",
1070 help = "Destination CIDR routed through the Hetzner vSwitch gateway"
1071 )]
1072 route_cidr: String,
1073 #[arg(long, help = "Apply or activate the new config immediately")]
1074 apply: bool,
1075 #[arg(long, help = "Preview file changes without writing them")]
1076 dry_run: bool,
1077 },
1078}
1079
1080#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
1081pub enum NginxDnsMode {
1082 Manual,
1083 Plugin,
1084}
1085
1086#[derive(Subcommand, Debug)]
1087pub enum NginxSubCommand {
1088 #[command(
1089 about = "Provision an HTTPS NGINX reverse proxy with Certbot",
1090 long_about = "Provision an NGINX reverse proxy, issue or reuse Let's Encrypt certificates,\n\
1091and write final HTTP->HTTPS redirect + TLS proxy config.\n\
1092\n\
1093Wildcard domains (for example *.example.com) require DNS-01 mode.\n\
1094Use --dns-mode manual for interactive TXT record prompts, or --dns-mode plugin\n\
1095with --dns-plugin and --dns-creds for non-interactive provider automation."
1096 )]
1097 Setup {
1098 #[arg(short, long, help = "Domain name (supports wildcard: *.example.com)")]
1099 domain: String,
1100 #[arg(short, long, help = "Port to proxy to")]
1101 port: u16,
1102 #[arg(
1103 short,
1104 long,
1105 help = "Email used for Let's Encrypt account registration"
1106 )]
1107 email: String,
1108 #[arg(
1109 long,
1110 value_enum,
1111 default_value_t = NginxDnsMode::Manual,
1112 help = "DNS challenge mode for wildcard certificates: manual or plugin"
1113 )]
1114 dns_mode: NginxDnsMode,
1115 #[arg(
1116 long,
1117 help = "Certbot DNS plugin name for --dns-mode plugin (for example: cloudflare)"
1118 )]
1119 dns_plugin: Option<String>,
1120 #[arg(
1121 long,
1122 help = "Path to DNS plugin credentials file for --dns-mode plugin"
1123 )]
1124 dns_creds: Option<PathBuf>,
1125 #[arg(
1126 long,
1127 default_value_t = true,
1128 action = clap::ArgAction::Set,
1129 value_parser = clap::builder::BoolishValueParser::new(),
1130 help = "For wildcard domains, also request the base domain certificate (true|false)"
1131 )]
1132 include_base: bool,
1133 },
1134 #[command(about = "List discovered NGINX sites with listen/upstream ports")]
1135 List,
1136 #[command(about = "Show full NGINX config for one domain or all domains")]
1137 Show {
1138 #[arg(help = "Optional domain name to inspect")]
1139 domain: Option<String>,
1140 },
1141 #[command(about = "Open an NGINX site config in your configured editor")]
1142 Edit {
1143 #[arg(help = "Domain name to edit")]
1144 domain: String,
1145 },
1146 #[command(about = "Update upstream port for an existing NGINX site")]
1147 Update {
1148 #[arg(short, long, help = "Domain name to update")]
1149 domain: String,
1150 #[arg(short, long, help = "New port to proxy to")]
1151 port: u16,
1152 },
1153}
1154
1155#[derive(Args, Debug)]
1156#[command(
1157 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1158 after_help = crate::cli::help_render::DIAG_AFTER_HELP
1159)]
1160pub struct DiagCmd {
1161 #[arg(long, help = "Check Nginx configuration")]
1162 pub nginx: bool,
1163 #[arg(long, hide = true)]
1164 pub refresh_system_inventory: bool,
1165 #[arg(
1166 long,
1167 help = "Refresh and print persisted machine inventory in the global XBP config"
1168 )]
1169 pub codetime: bool,
1170 #[arg(
1171 long,
1172 help = "Inspect Cursor roaming data on Windows; implies --codetime"
1173 )]
1174 pub cursor: bool,
1175 #[arg(long, help = "Check specific ports (comma-separated)")]
1176 pub ports: Option<String>,
1177 #[arg(long, help = "Skip internet speed test")]
1178 pub no_speed_test: bool,
1179 #[arg(
1180 long,
1181 help = "Path to docker compose file to validate (defaults to docker-compose.yml/compose.yml)"
1182 )]
1183 pub compose_file: Option<String>,
1184}
1185
1186#[derive(Args, Debug)]
1187pub struct MonitorCmd {
1188 #[command(subcommand)]
1189 pub command: Option<MonitorSubCommand>,
1190}
1191
1192#[derive(Subcommand, Debug)]
1193pub enum MonitorSubCommand {
1194 Check,
1195 Start,
1196}
1197
1198#[cfg(feature = "monitoring")]
1199#[derive(Args, Debug)]
1200pub struct MonitoringCmd {
1201 #[command(subcommand)]
1202 pub command: MonitoringSubCommand,
1203}
1204
1205#[cfg(feature = "monitoring")]
1206#[derive(Subcommand, Debug)]
1207pub enum MonitoringSubCommand {
1208 Serve {
1209 #[arg(
1210 short,
1211 long,
1212 default_value = "prodzilla.yml",
1213 help = "Monitoring config file"
1214 )]
1215 file: String,
1216 },
1217 RunOnce {
1218 #[arg(
1219 short,
1220 long,
1221 default_value = "prodzilla.yml",
1222 help = "Monitoring config file"
1223 )]
1224 file: String,
1225 #[arg(long, help = "Run probes only")]
1226 probes_only: bool,
1227 #[arg(long, help = "Run stories only")]
1228 stories_only: bool,
1229 },
1230 List {
1231 #[arg(
1232 short,
1233 long,
1234 default_value = "prodzilla.yml",
1235 help = "Monitoring config file"
1236 )]
1237 file: String,
1238 },
1239}
1240
1241#[derive(Args, Debug)]
1242#[command(
1243 arg_required_else_help = true,
1244 disable_help_subcommand = true,
1245 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1246 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."
1247)]
1248pub struct ApiCmd {
1249 #[command(subcommand)]
1250 pub command: ApiSubCommand,
1251}
1252
1253#[derive(Args, Debug)]
1254#[command(
1255 arg_required_else_help = true,
1256 disable_help_subcommand = true,
1257 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1258 after_help = "Examples:\n xbp mcp serve\n xbp mcp serve --detach\n xbp mcp config\n xbp mcp inspector\n xbp mcp inspector --launch\n xbp mcp status\n xbp mcp install --port 1113\n\nCursor MCP url: http://127.0.0.1:1113/sse"
1259)]
1260pub struct McpCmd {
1261 #[command(subcommand)]
1262 pub command: McpSubCommand,
1263}
1264
1265#[derive(Subcommand, Debug)]
1266pub enum McpSubCommand {
1267 #[command(about = "Start the MCP HTTP/SSE server (foreground or detached)")]
1268 Serve(McpServeCmd),
1269 #[command(about = "Install and enable xbp-mcp.service via systemd")]
1270 Install(McpInstallCmd),
1271 #[command(about = "Check whether the MCP server is reachable")]
1272 Status(McpStatusCmd),
1273 #[command(about = "Print Cursor MCP configuration JSON")]
1274 Config(McpConfigCmd),
1275 #[command(about = "Print prefilled MCP Inspector URLs and tool-call presets")]
1276 Inspector(McpInspectorCmd),
1277}
1278
1279#[derive(Args, Debug)]
1280pub struct McpServeCmd {
1281 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1282 pub bind: String,
1283 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1284 pub port: u16,
1285 #[arg(
1286 long,
1287 help = "Spawn a background MCP server process and print Cursor config"
1288 )]
1289 pub detach: bool,
1290 #[arg(long, help = "Use stdio MCP transport instead of HTTP/SSE")]
1291 pub stdio: bool,
1292}
1293
1294#[derive(Args, Debug)]
1295pub struct McpInstallCmd {
1296 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1297 pub bind: String,
1298 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1299 pub port: u16,
1300}
1301
1302#[derive(Args, Debug)]
1303pub struct McpStatusCmd {
1304 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1305 pub bind: String,
1306 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1307 pub port: u16,
1308}
1309
1310#[derive(Args, Debug)]
1311pub struct McpConfigCmd {
1312 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1313 pub bind: String,
1314 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1315 pub port: u16,
1316}
1317
1318#[derive(Args, Debug)]
1319pub struct McpInspectorCmd {
1320 #[arg(
1321 long,
1322 default_value = "127.0.0.1",
1323 help = "Bind address for the xbp MCP server"
1324 )]
1325 pub bind: String,
1326 #[arg(
1327 long,
1328 default_value_t = 1113,
1329 help = "HTTP port for the xbp MCP SSE transport"
1330 )]
1331 pub port: u16,
1332 #[arg(long, default_value_t = 6274, help = "MCP Inspector UI port")]
1333 pub inspector_port: u16,
1334 #[arg(
1335 long,
1336 help = "Write .xbp/mcp-inspector.json for `npx @modelcontextprotocol/inspector --config`"
1337 )]
1338 pub write_config: bool,
1339 #[arg(
1340 long,
1341 help = "Launch MCP Inspector via npx (writes config first; requires Node.js)"
1342 )]
1343 pub launch: bool,
1344 #[arg(long, help = "Emit machine-readable JSON playbook")]
1345 pub json: bool,
1346}
1347
1348#[derive(Args, Debug, Clone, Default)]
1349pub struct ApiTargetOptions {
1350 #[arg(long, help = "Override the request base URL for this command")]
1351 pub base_url: Option<String>,
1352 #[arg(
1353 long,
1354 help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
1355 )]
1356 pub web: bool,
1357 #[arg(
1358 long,
1359 help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
1360 )]
1361 pub no_auth: bool,
1362 #[arg(
1363 long,
1364 help = "Extra header in 'Name: Value' format",
1365 value_name = "HEADER"
1366 )]
1367 pub header: Vec<String>,
1368 #[arg(long, help = "Print response headers")]
1369 pub include_headers: bool,
1370 #[arg(
1371 long,
1372 help = "Print the response body as-is without JSON pretty formatting"
1373 )]
1374 pub raw: bool,
1375}
1376
1377#[cfg(feature = "docker")]
1378#[derive(Args, Debug)]
1379pub struct DockerCmd {
1380 #[arg(
1381 trailing_var_arg = true,
1382 allow_hyphen_values = true,
1383 help = "Arguments to pass directly to the Docker CLI (default: --help)"
1384 )]
1385 pub args: Vec<String>,
1386}
1387
1388#[derive(Subcommand, Debug)]
1389pub enum ApiSubCommand {
1390 #[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
1391 Install {
1392 #[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
1393 port: u16,
1394 },
1395 #[command(about = "Call the XBP API health endpoint")]
1396 Health(ApiHealthCmd),
1397 #[command(about = "Manage XBP control-plane projects")]
1398 Projects(ApiProjectsCmd),
1399 #[command(about = "Manage XBP daemon registrations and heartbeats")]
1400 Daemons(ApiDaemonsCmd),
1401 #[command(about = "Manage XBP deployment jobs")]
1402 Jobs(ApiJobsCmd),
1403 #[command(about = "Manage XBP proxy routes on the local API server")]
1404 Routes(ApiRoutesCmd),
1405 #[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
1406 Request(ApiRequestCmd),
1407}
1408
1409#[derive(Args, Debug)]
1410pub struct ApiHealthCmd {
1411 #[command(flatten)]
1412 pub target: ApiTargetOptions,
1413}
1414
1415#[derive(Args, Debug)]
1416pub struct ApiProjectsCmd {
1417 #[command(subcommand)]
1418 pub command: ApiProjectsSubCommand,
1419}
1420
1421#[derive(Subcommand, Debug)]
1422pub enum ApiProjectsSubCommand {
1423 #[command(about = "List projects from the XBP control-plane API")]
1424 List(ApiProjectsListCmd),
1425 #[command(about = "Create or upsert a control-plane project")]
1426 Create(Box<ApiProjectsCreateCmd>),
1427}
1428
1429#[derive(Args, Debug)]
1430pub struct ApiProjectsListCmd {
1431 #[arg(long, help = "Optional organization ID filter")]
1432 pub organization_id: Option<String>,
1433 #[command(flatten)]
1434 pub target: ApiTargetOptions,
1435}
1436
1437#[derive(Args, Debug)]
1438pub struct ApiProjectsCreateCmd {
1439 #[arg(long, help = "Project name")]
1440 pub name: String,
1441 #[arg(long, help = "Project path or repo path key")]
1442 pub path: String,
1443 #[arg(long, help = "Optional organization ID")]
1444 pub organization_id: Option<String>,
1445 #[arg(long, help = "Optional project slug")]
1446 pub slug: Option<String>,
1447 #[arg(long, help = "Optional project version")]
1448 pub version: Option<String>,
1449 #[arg(long, help = "Optional build directory")]
1450 pub build_dir: Option<String>,
1451 #[arg(long, help = "Optional runtime enum value")]
1452 pub runtime: Option<String>,
1453 #[arg(long, help = "Optional default branch")]
1454 pub default_branch: Option<String>,
1455 #[arg(long, help = "Optional repository root directory")]
1456 pub root_directory: Option<String>,
1457 #[arg(long, help = "Optional build command")]
1458 pub build_command: Option<String>,
1459 #[arg(long, help = "Optional install command")]
1460 pub install_command: Option<String>,
1461 #[arg(long, help = "Optional start command")]
1462 pub start_command: Option<String>,
1463 #[arg(long, help = "Optional output directory")]
1464 pub output_directory: Option<String>,
1465 #[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
1466 pub repository_json: Option<String>,
1467 #[arg(long, help = "Runtime policy JSON payload")]
1468 pub runtime_policy_json: Option<String>,
1469 #[arg(long, help = "Metadata JSON object")]
1470 pub metadata_json: Option<String>,
1471 #[command(flatten)]
1472 pub target: ApiTargetOptions,
1473}
1474
1475#[derive(Args, Debug)]
1476pub struct ApiDaemonsCmd {
1477 #[command(subcommand)]
1478 pub command: ApiDaemonsSubCommand,
1479}
1480
1481#[derive(Subcommand, Debug)]
1482pub enum ApiDaemonsSubCommand {
1483 #[command(about = "List registered daemons")]
1484 List(ApiDaemonsListCmd),
1485 #[command(about = "Register or upsert a daemon record")]
1486 Register(ApiDaemonsRegisterCmd),
1487 #[command(about = "Post a heartbeat update for a daemon")]
1488 Heartbeat(ApiDaemonsHeartbeatCmd),
1489 #[command(about = "Update daemon status only")]
1490 UpdateStatus(ApiDaemonsUpdateStatusCmd),
1491}
1492
1493#[derive(Args, Debug)]
1494pub struct ApiDaemonsListCmd {
1495 #[command(flatten)]
1496 pub target: ApiTargetOptions,
1497}
1498
1499#[derive(Args, Debug)]
1500pub struct ApiDaemonsRegisterCmd {
1501 #[arg(long, help = "Daemon node name")]
1502 pub node_name: String,
1503 #[arg(long, help = "Daemon hostname")]
1504 pub hostname: String,
1505 #[arg(long, help = "Daemon binary version")]
1506 pub version: String,
1507 #[arg(long, help = "Optional region")]
1508 pub region: Option<String>,
1509 #[arg(long, help = "Optional public IP")]
1510 pub public_ip: Option<String>,
1511 #[arg(long, help = "Optional internal IP")]
1512 pub internal_ip: Option<String>,
1513 #[arg(long, help = "Optional status enum value")]
1514 pub status: Option<String>,
1515 #[arg(long, help = "Optional CPU core count")]
1516 pub cpu_cores: Option<i32>,
1517 #[arg(long, help = "Optional total memory in MB")]
1518 pub memory_total_mb: Option<i32>,
1519 #[arg(long, help = "Optional total disk in GB")]
1520 pub disk_total_gb: Option<i32>,
1521 #[arg(long, help = "Labels JSON object")]
1522 pub labels_json: Option<String>,
1523 #[arg(long, help = "Metadata JSON object")]
1524 pub metadata_json: Option<String>,
1525 #[command(flatten)]
1526 pub target: ApiTargetOptions,
1527}
1528
1529#[derive(Args, Debug)]
1530pub struct ApiDaemonsHeartbeatCmd {
1531 #[arg(help = "Daemon ID")]
1532 pub daemon_id: String,
1533 #[arg(long, help = "Optional status enum value")]
1534 pub status: Option<String>,
1535 #[arg(long, help = "Optional daemon version")]
1536 pub version: Option<String>,
1537 #[arg(long, help = "Optional public IP")]
1538 pub public_ip: Option<String>,
1539 #[arg(long, help = "Optional internal IP")]
1540 pub internal_ip: Option<String>,
1541 #[arg(long, help = "Optional CPU core count")]
1542 pub cpu_cores: Option<i32>,
1543 #[arg(long, help = "Optional total memory in MB")]
1544 pub memory_total_mb: Option<i32>,
1545 #[arg(long, help = "Optional total disk in GB")]
1546 pub disk_total_gb: Option<i32>,
1547 #[arg(long, help = "Labels JSON object")]
1548 pub labels_json: Option<String>,
1549 #[command(flatten)]
1550 pub target: ApiTargetOptions,
1551}
1552
1553#[derive(Args, Debug)]
1554pub struct ApiDaemonsUpdateStatusCmd {
1555 #[arg(help = "Daemon ID")]
1556 pub daemon_id: String,
1557 #[arg(long, help = "Daemon status enum value")]
1558 pub status: String,
1559 #[command(flatten)]
1560 pub target: ApiTargetOptions,
1561}
1562
1563#[derive(Args, Debug)]
1564pub struct ApiJobsCmd {
1565 #[command(subcommand)]
1566 pub command: ApiJobsSubCommand,
1567}
1568
1569#[derive(Subcommand, Debug)]
1570pub enum ApiJobsSubCommand {
1571 #[command(about = "List deployment jobs")]
1572 List(ApiJobsListCmd),
1573 #[command(about = "Create a deployment job for a project")]
1574 Create(ApiJobsCreateCmd),
1575 #[command(about = "Claim the next deployment job for a daemon")]
1576 Claim(ApiJobsClaimCmd),
1577 #[command(about = "Update deployment job status")]
1578 Update(ApiJobsUpdateCmd),
1579}
1580
1581#[derive(Args, Debug)]
1582pub struct ApiJobsListCmd {
1583 #[arg(long, help = "Optional project ID filter")]
1584 pub project_id: Option<String>,
1585 #[arg(long, help = "Optional deployment ID filter")]
1586 pub deployment_id: Option<String>,
1587 #[arg(long, help = "Optional daemon ID filter")]
1588 pub daemon_id: Option<String>,
1589 #[arg(long, help = "Optional status filter")]
1590 pub status: Option<String>,
1591 #[arg(long, help = "Optional result limit")]
1592 pub limit: Option<usize>,
1593 #[command(flatten)]
1594 pub target: ApiTargetOptions,
1595}
1596
1597#[derive(Args, Debug)]
1598pub struct ApiJobsCreateCmd {
1599 #[arg(long, help = "Project ID")]
1600 pub project_id: String,
1601 #[arg(long, help = "Deployment ID")]
1602 pub deployment_id: String,
1603 #[arg(long, help = "Optional daemon ID assignment")]
1604 pub daemon_id: Option<String>,
1605 #[arg(long, help = "Optional priority")]
1606 pub priority: Option<i32>,
1607 #[arg(long, help = "Optional max attempts")]
1608 pub max_attempts: Option<i32>,
1609 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1610 pub run_after: Option<String>,
1611 #[arg(long, help = "Optional payload JSON object")]
1612 pub payload_json: Option<String>,
1613 #[command(flatten)]
1614 pub target: ApiTargetOptions,
1615}
1616
1617#[derive(Args, Debug)]
1618pub struct ApiJobsClaimCmd {
1619 #[arg(long, help = "Daemon ID claiming work")]
1620 pub daemon_id: String,
1621 #[arg(long, help = "Optional lock owner")]
1622 pub locked_by: Option<String>,
1623 #[command(flatten)]
1624 pub target: ApiTargetOptions,
1625}
1626
1627#[derive(Args, Debug)]
1628pub struct ApiJobsUpdateCmd {
1629 #[arg(help = "Deployment job ID")]
1630 pub job_id: String,
1631 #[arg(long, help = "Deployment job status enum value")]
1632 pub status: String,
1633 #[arg(long, help = "Optional error text")]
1634 pub error_text: Option<String>,
1635 #[command(flatten)]
1636 pub target: ApiTargetOptions,
1637}
1638
1639#[derive(Args, Debug)]
1640pub struct ApiRoutesCmd {
1641 #[command(subcommand)]
1642 pub command: ApiRoutesSubCommand,
1643}
1644
1645#[derive(Subcommand, Debug)]
1646pub enum ApiRoutesSubCommand {
1647 #[command(about = "List configured proxy routes")]
1648 List(ApiRoutesListCmd),
1649 #[command(about = "Create or replace a proxy route")]
1650 Create(ApiRoutesCreateCmd),
1651 #[command(about = "Delete a proxy route by domain")]
1652 Delete(ApiRoutesDeleteCmd),
1653}
1654
1655#[derive(Args, Debug)]
1656pub struct ApiRoutesListCmd {
1657 #[command(flatten)]
1658 pub target: ApiTargetOptions,
1659}
1660
1661#[derive(Args, Debug)]
1662pub struct ApiRoutesCreateCmd {
1663 #[arg(long, help = "Domain name for the route")]
1664 pub domain: String,
1665 #[arg(long, help = "Upstream target URL", required = true)]
1666 pub target: Vec<String>,
1667 #[arg(
1668 long,
1669 help = "Weighted upstream target in url=weight form",
1670 value_name = "URL=WEIGHT"
1671 )]
1672 pub weighted_target: Vec<String>,
1673 #[arg(long, help = "Optional header condition")]
1674 pub header_condition: Option<String>,
1675 #[arg(long, help = "Optional path prefix condition")]
1676 pub path_prefix: Option<String>,
1677 #[command(flatten)]
1678 pub target_options: ApiTargetOptions,
1679}
1680
1681#[derive(Args, Debug)]
1682pub struct ApiRoutesDeleteCmd {
1683 #[arg(help = "Domain name for the route")]
1684 pub domain: String,
1685 #[command(flatten)]
1686 pub target: ApiTargetOptions,
1687}
1688
1689#[derive(Args, Debug)]
1690pub struct ApiRequestCmd {
1691 #[arg(help = "Request path like /projects or a full https:// URL")]
1692 pub path: String,
1693 #[arg(
1694 short = 'X',
1695 long,
1696 help = "HTTP method to use (default: GET, or POST when a body is provided)"
1697 )]
1698 pub method: Option<String>,
1699 #[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
1700 pub body: Option<String>,
1701 #[arg(long, help = "Read the request body from a file")]
1702 pub body_file: Option<PathBuf>,
1703 #[command(flatten)]
1704 pub target: ApiTargetOptions,
1705}
1706#[derive(Args, Debug)]
1707pub struct TailCmd {
1708 #[arg(long, help = "Tail Kafka topic instead of log files")]
1709 pub kafka: bool,
1710 #[arg(long, help = "Ship logs to Kafka")]
1711 pub ship: bool,
1712}
1713
1714#[derive(Args, Debug)]
1715#[command(
1716 arg_required_else_help = true,
1717 disable_help_subcommand = true,
1718 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1719 after_help = crate::cli::help_render::GENERATE_AFTER_HELP
1720)]
1721pub struct GenerateCmd {
1722 #[command(subcommand)]
1723 pub command: GenerateSubCommand,
1724}
1725
1726#[derive(Subcommand, Debug)]
1727pub enum GenerateSubCommand {
1728 #[command(about = "Generate or update .xbp/xbp.yaml (and convert legacy JSON)")]
1729 Config(GenerateConfigCmd),
1730 Systemd(GenerateSystemdCmd),
1731}
1732
1733#[derive(Args, Debug)]
1734pub struct GenerateConfigCmd {
1735 #[arg(
1736 long,
1737 help = "Overwrite .xbp/xbp.yaml if it already exists (default errors when present)"
1738 )]
1739 pub force: bool,
1740 #[arg(
1741 long,
1742 help = "Refresh .xbp/xbp.yaml by applying project detection defaults for missing fields"
1743 )]
1744 pub update: bool,
1745 #[arg(
1746 long,
1747 help = "Path to a legacy xbp.json file to convert into .xbp/xbp.yaml"
1748 )]
1749 pub from_json: Option<PathBuf>,
1750}
1751
1752#[derive(Args, Debug)]
1753#[command(
1754 arg_required_else_help = true,
1755 disable_help_subcommand = true,
1756 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1757 after_help = "Examples:\n xbp workers list\n xbp workers ls\n xbp workers logs -f\n xbp workers logs --build --failed\n xbp workers secrets put --environment production --name GITHUB_APP_PRIVATE_KEY --from-stdin\n xbp workers secrets list --environment production\n xbp workers settings get --environment production\n xbp workers wrangler generate-config --output wrangler.deploy.json\n xbp workers d1 migrations apply DB --remote\n xbp workers deploy sync-env-local\n xbp workers deploy ci --version-upload\n xbp workers worktree link-dev-vars"
1758)]
1759pub struct WorkersCmd {
1760 #[arg(
1761 long,
1762 help = "Workers project root (defaults to current dir, or apps/web inside the current XBP repo when present)"
1763 )]
1764 pub root: Option<PathBuf>,
1765 #[arg(long, help = "Cloudflare API token override")]
1766 pub token: Option<String>,
1767 #[arg(long, help = "Cloudflare account ID override")]
1768 pub account_id: Option<String>,
1769 #[command(subcommand)]
1770 pub command: WorkersSubCommand,
1771}
1772
1773#[derive(Subcommand, Debug)]
1774pub enum WorkersSubCommand {
1775 #[command(
1776 alias = "ls",
1777 about = "List Cloudflare Worker scripts with build and placement status",
1778 after_help = "Examples:\n xbp workers list\n xbp workers list --all\n xbp workers list --json"
1779 )]
1780 List(WorkersListCmd),
1781 #[command(
1782 about = "Show deployment status, stream runtime logs, or fetch Workers Builds CI logs",
1783 after_help = "Examples:\n xbp workers logs\n xbp workers logs -f\n xbp workers logs xbp-production\n xbp workers logs --build\n xbp workers logs --build --failed"
1784 )]
1785 Logs(WorkersLogsCmd),
1786 #[command(about = "Manage secret bindings for a Worker script or Wrangler environment")]
1787 Secrets(WorkersSecretsCmd),
1788 #[command(about = "Fetch remote Worker settings via the Cloudflare API")]
1789 Settings(WorkersSettingsCmd),
1790 #[command(about = "Inspect or generate Wrangler config helpers")]
1791 Wrangler(WorkersWranglerCmd),
1792 #[command(about = "Run Wrangler D1 migration helpers")]
1793 D1(WorkersD1Cmd),
1794 #[command(about = "Run Worker deploy and predeploy helpers")]
1795 Deploy(WorkersDeployCmd),
1796 #[command(about = "Inspect shared worktree paths or link shared dev files")]
1797 Worktree(WorkersWorktreeCmd),
1798 #[command(about = "Show resolved Worker runtime metadata from local env and config files")]
1799 Env(WorkersEnvCmd),
1800}
1801
1802#[derive(Args, Debug, Default)]
1803pub struct WorkersListCmd {
1804 #[arg(
1805 long,
1806 help = "Show every Worker in the account instead of only Workers for this XBP project"
1807 )]
1808 pub all: bool,
1809 #[arg(long, help = "Output the worker inventory as JSON")]
1810 pub json: bool,
1811}
1812
1813#[derive(Args, Debug)]
1814pub struct WorkersLogsCmd {
1815 #[command(flatten)]
1816 pub target: WorkersTargetArgs,
1817 #[arg(
1818 short = 'f',
1819 long = "follow",
1820 help = "Stream runtime logs until interrupted (wrangler tail)"
1821 )]
1822 pub follow: bool,
1823 #[arg(
1824 long = "build",
1825 help = "Show Workers Builds CI logs instead of runtime deployment output"
1826 )]
1827 pub build: bool,
1828 #[arg(
1829 long = "failed",
1830 help = "When using --build, prefer the latest failed build"
1831 )]
1832 pub failed: bool,
1833 #[arg(long, help = "Output logs as JSON")]
1834 pub json: bool,
1835 #[arg(help = "Worker script name. When omitted, xbp prompts interactively.")]
1836 pub script_name: Option<String>,
1837}
1838
1839#[derive(Args, Debug, Clone, Default)]
1840pub struct WorkersTargetArgs {
1841 #[arg(
1842 long,
1843 help = "Worker base name (defaults to wrangler config name or xbp)"
1844 )]
1845 pub worker: Option<String>,
1846 #[arg(
1847 long = "environment",
1848 alias = "env",
1849 help = "Wrangler environment name. The remote script resolves to <worker>-<environment>."
1850 )]
1851 pub environment: Option<String>,
1852 #[arg(
1853 long,
1854 help = "Exact remote script name override. When set, this bypasses <worker>-<environment> resolution."
1855 )]
1856 pub script: Option<String>,
1857}
1858
1859#[derive(Args, Debug)]
1860pub struct WorkersSecretsCmd {
1861 #[command(flatten)]
1862 pub target: WorkersTargetArgs,
1863 #[command(subcommand)]
1864 pub command: WorkersSecretsSubCommand,
1865}
1866
1867#[derive(Subcommand, Debug)]
1868pub enum WorkersSecretsSubCommand {
1869 #[command(about = "List secret bindings on the resolved Worker script")]
1870 List(WorkersSecretsListCmd),
1871 #[command(about = "Fetch one secret binding metadata or value")]
1872 Get(WorkersSecretsGetCmd),
1873 #[command(about = "Create or update a secret binding")]
1874 Put(WorkersSecretsPutCmd),
1875 #[command(about = "Delete a secret binding")]
1876 Delete(WorkersSecretsDeleteCmd),
1877 #[command(about = "Create, update, or delete multiple secret bindings from a file")]
1878 Bulk(WorkersSecretsBulkCmd),
1879}
1880
1881#[derive(Args, Debug, Default)]
1882pub struct WorkersSecretsListCmd {}
1883
1884#[derive(Args, Debug)]
1885pub struct WorkersSecretsGetCmd {
1886 #[arg(long, help = "Secret binding name")]
1887 pub name: String,
1888}
1889
1890#[derive(Args, Debug)]
1891pub struct WorkersSecretsPutCmd {
1892 #[arg(long, help = "Secret binding name")]
1893 pub name: String,
1894 #[arg(long, help = "Secret value")]
1895 pub value: Option<String>,
1896 #[arg(long, help = "Read the secret value from stdin instead of --value")]
1897 pub from_stdin: bool,
1898}
1899
1900#[derive(Args, Debug)]
1901pub struct WorkersSecretsDeleteCmd {
1902 #[arg(long, help = "Secret binding name")]
1903 pub name: String,
1904}
1905
1906#[derive(Args, Debug)]
1907pub struct WorkersSecretsBulkCmd {
1908 #[arg(long, help = "Path to a .env or JSON file containing secret updates")]
1909 pub file: PathBuf,
1910 #[arg(
1911 long,
1912 default_value = "env",
1913 help = "Input format: env or json. For json, pass an object mapping names to string values or null for deletes."
1914 )]
1915 pub format: String,
1916}
1917
1918#[derive(Args, Debug)]
1919pub struct WorkersSettingsCmd {
1920 #[command(flatten)]
1921 pub target: WorkersTargetArgs,
1922}
1923
1924#[derive(Args, Debug)]
1925pub struct WorkersWranglerCmd {
1926 #[command(subcommand)]
1927 pub command: WorkersWranglerSubCommand,
1928}
1929
1930#[derive(Subcommand, Debug)]
1931pub enum WorkersWranglerSubCommand {
1932 #[command(about = "Generate a Wrangler deploy config JSON file from env vars")]
1933 GenerateConfig(WorkersWranglerGenerateConfigCmd),
1934 #[command(about = "Resolve which Wrangler config file local dev should use")]
1935 ConfigPath(WorkersWranglerConfigPathCmd),
1936}
1937
1938#[derive(Args, Debug)]
1939pub struct WorkersWranglerGenerateConfigCmd {
1940 #[arg(
1941 long,
1942 default_value = "wrangler.deploy.json",
1943 help = "Output filename, relative to the worker root unless absolute"
1944 )]
1945 pub output: PathBuf,
1946}
1947
1948#[derive(Args, Debug)]
1949pub struct WorkersWranglerConfigPathCmd {
1950 #[arg(
1951 long,
1952 default_value = "serve",
1953 help = "Calling command name, for example serve"
1954 )]
1955 pub command_name: String,
1956 #[arg(
1957 long,
1958 default_value = "development",
1959 help = "Execution mode, for example development or production"
1960 )]
1961 pub mode: String,
1962}
1963
1964#[derive(Args, Debug)]
1965pub struct WorkersD1Cmd {
1966 #[command(subcommand)]
1967 pub command: WorkersD1SubCommand,
1968}
1969
1970#[derive(Subcommand, Debug)]
1971pub enum WorkersD1SubCommand {
1972 #[command(about = "Apply pending Wrangler D1 migrations")]
1973 Migrations(WorkersD1MigrationsCmd),
1974}
1975
1976#[derive(Args, Debug)]
1977pub struct WorkersD1MigrationsCmd {
1978 #[command(subcommand)]
1979 pub command: WorkersD1MigrationsSubCommand,
1980}
1981
1982#[derive(Subcommand, Debug)]
1983pub enum WorkersD1MigrationsSubCommand {
1984 #[command(about = "Apply pending migrations to a local or remote D1 database")]
1985 Apply(WorkersD1MigrationsApplyCmd),
1986}
1987
1988#[derive(Args, Debug)]
1989pub struct WorkersD1MigrationsApplyCmd {
1990 #[arg(help = "D1 database binding or name, for example DB")]
1991 pub database: String,
1992 #[arg(
1993 long,
1994 conflicts_with = "remote",
1995 help = "Apply migrations to the local Wrangler D1 database"
1996 )]
1997 pub local: bool,
1998 #[arg(
1999 long,
2000 conflicts_with = "local",
2001 help = "Apply migrations to the remote D1 database"
2002 )]
2003 pub remote: bool,
2004 #[arg(long, help = "Wrangler config path override")]
2005 pub config: Option<PathBuf>,
2006 #[command(flatten)]
2007 pub target: WorkersTargetArgs,
2008 #[arg(
2009 long,
2010 help = "Persist local D1 state to this directory. When omitted in a git worktree, xbp uses the shared .wrangler/state path automatically."
2011 )]
2012 pub persist_to: Option<PathBuf>,
2013 #[arg(
2014 long,
2015 help = "Disable the automatic shared .wrangler/state path when running local migrations from a git worktree"
2016 )]
2017 pub no_shared_worktree_state: bool,
2018}
2019
2020#[derive(Args, Debug)]
2021pub struct WorkersDeployCmd {
2022 #[command(subcommand)]
2023 pub command: WorkersDeploySubCommand,
2024}
2025
2026#[derive(Subcommand, Debug)]
2027pub enum WorkersDeploySubCommand {
2028 #[command(about = "Run the predeploy sync flow unless Workers CI mode is active")]
2029 Predeploy(WorkersDeployPredeployCmd),
2030 #[command(about = "Read .env.local or process env, then emit .dev.vars and Wrangler configs")]
2031 SyncEnvLocal(WorkersDeploySyncEnvLocalCmd),
2032 #[command(about = "Run the existing Cloudflare CI deploy workflow")]
2033 Ci(WorkersDeployCiCmd),
2034 #[command(
2035 about = "Run the existing deploy-selection flow that chooses CI or local deploy behavior"
2036 )]
2037 Select(WorkersDeploySelectCmd),
2038}
2039
2040#[derive(Args, Debug)]
2041pub struct WorkersDeployPredeployCmd {
2042 #[arg(long, help = "Force Workers CI mode and skip local sync")]
2043 pub ci: bool,
2044}
2045
2046#[derive(Args, Debug)]
2047pub struct WorkersDeploySyncEnvLocalCmd {}
2048
2049#[derive(Args, Debug)]
2050pub struct WorkersDeployCiCmd {
2051 #[arg(long, help = "Upload a new version without immediately deploying it")]
2052 pub version_upload: bool,
2053}
2054
2055#[derive(Args, Debug)]
2056pub struct WorkersDeploySelectCmd {
2057 #[arg(long, help = "Force the WORKERS_CI=1 branch of the selector")]
2058 pub ci: bool,
2059 #[arg(
2060 long,
2061 help = "Branch name to expose as WORKERS_CI_BRANCH when --ci is set"
2062 )]
2063 pub branch: Option<String>,
2064}
2065
2066#[derive(Args, Debug)]
2067pub struct WorkersWorktreeCmd {
2068 #[command(subcommand)]
2069 pub command: WorkersWorktreeSubCommand,
2070}
2071
2072#[derive(Subcommand, Debug)]
2073pub enum WorkersWorktreeSubCommand {
2074 #[command(about = "Print repo-root, primary worktree, and shared Wrangler state paths")]
2075 Paths(WorkersWorktreePathsCmd),
2076 #[command(
2077 about = "Symlink apps/web/.dev.vars and wrangler.dev.jsonc from the primary worktree when in a linked worktree"
2078 )]
2079 LinkDevVars(WorkersWorktreeLinkDevVarsCmd),
2080}
2081
2082#[derive(Args, Debug, Default)]
2083pub struct WorkersWorktreePathsCmd {}
2084
2085#[derive(Args, Debug, Default)]
2086pub struct WorkersWorktreeLinkDevVarsCmd {}
2087
2088#[derive(Args, Debug)]
2089pub struct WorkersEnvCmd {
2090 #[command(flatten)]
2091 pub target: WorkersTargetArgs,
2092 #[arg(
2093 long,
2094 help = "Show resolved plain-text binding values instead of masking them"
2095 )]
2096 pub show_values: bool,
2097}
2098
2099#[cfg(feature = "secrets")]
2100#[derive(Args, Debug)]
2101pub struct SecretsCmd {
2102 #[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
2103 pub provider: SecretsProviderKind,
2104 #[arg(long, help = "GitHub repository override (owner/repo)")]
2105 pub repo: Option<String>,
2106 #[arg(
2107 long,
2108 help = "Provider token override (GitHub token or Cloudflare API token)"
2109 )]
2110 pub token: Option<String>,
2111 #[arg(long, help = "Cloudflare account ID override")]
2112 pub account_id: Option<String>,
2113 #[arg(
2114 long = "environment",
2115 alias = "env",
2116 default_value = "xbp-dev",
2117 help = "Environment to sync (default: xbp-dev). Nested services are scoped automatically, e.g. xbp-dev-web."
2118 )]
2119 pub environment: String,
2120 #[arg(
2121 long,
2122 help = "Service name from .xbp/xbp.yaml. If omitted, XBP resolves it from the current directory or prompts when ambiguous."
2123 )]
2124 pub service: Option<String>,
2125 #[command(subcommand)]
2126 pub command: Option<SecretsSubCommand>,
2127}
2128
2129#[cfg(feature = "secrets")]
2130#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2131pub enum SecretsProviderKind {
2132 Github,
2133 Cloudflare,
2134 Railway,
2135 Vercel,
2136}
2137
2138#[cfg(feature = "secrets")]
2139#[derive(Subcommand, Debug)]
2140pub enum SecretsSubCommand {
2141 #[command(alias = "ls", alias = "list-providers")]
2143 Providers,
2144 List(ListCmd),
2146 Push(PushCmd),
2148 Pull(PullCmd),
2150 GenerateDefault(GenerateDefaultCmd),
2152 GenerateExample(GenerateExampleCmd),
2154 Diff,
2156 Verify,
2158 #[command(name = "diag", alias = "doctor")]
2160 Diag,
2161 Stores(SecretsStoresCmd),
2163 Secrets(CloudflareSecretsCmd),
2165 Quota(SecretsQuotaCmd),
2167 #[command(name = "usage")]
2169 Usage,
2170}
2171
2172#[cfg(feature = "secrets")]
2173#[derive(Args, Debug)]
2174pub struct ListCmd {
2175 #[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
2176 pub file: Option<String>,
2177 #[arg(long, help = "Output format: plain (default) or json")]
2178 pub format: Option<String>,
2179}
2180
2181#[cfg(feature = "secrets")]
2182#[derive(Args, Debug)]
2183pub struct PushCmd {
2184 #[arg(long, help = "Path to env file (default: .env.local/.env)")]
2185 pub file: Option<String>,
2186 #[arg(
2187 long,
2188 help = "Force overwrite existing GitHub Actions environment variables"
2189 )]
2190 pub force: bool,
2191 #[arg(long, help = "Show what would be pushed without making changes")]
2192 pub dry_run: bool,
2193}
2194
2195#[cfg(feature = "secrets")]
2196#[derive(Args, Debug)]
2197pub struct PullCmd {
2198 #[arg(long, help = "Output file path (default: .env.local)")]
2199 pub output: Option<String>,
2200}
2201
2202#[cfg(feature = "secrets")]
2203#[derive(Args, Debug)]
2204pub struct GenerateDefaultCmd {
2205 #[arg(long, help = "Output file path (default: .env.default)")]
2206 pub output: Option<String>,
2207}
2208
2209#[cfg(feature = "secrets")]
2210#[derive(Args, Debug)]
2211pub struct GenerateExampleCmd {
2212 #[arg(long, help = "Output file path (default: .env.example)")]
2213 pub output: Option<String>,
2214 #[arg(long, help = "Remove keys from .env.local not in .env.example")]
2215 pub clean: bool,
2216 #[arg(long, help = "Only include vars matching prefix (repeatable)")]
2217 pub include_prefix: Vec<String>,
2218 #[arg(long, help = "Exclude vars matching prefix (repeatable)")]
2219 pub exclude_prefix: Vec<String>,
2220}
2221
2222#[cfg(feature = "secrets")]
2223#[derive(Args, Debug)]
2224pub struct SecretsStoresCmd {
2225 #[command(subcommand)]
2226 pub command: SecretsStoresSubCommand,
2227}
2228
2229#[cfg(feature = "secrets")]
2230#[derive(Subcommand, Debug)]
2231pub enum SecretsStoresSubCommand {
2232 List(CloudflareSecretsStoreListCmd),
2233 Get(CloudflareSecretsStoreGetCmd),
2234 Create(CloudflareSecretsStoreCreateCmd),
2235 Delete(CloudflareSecretsStoreDeleteCmd),
2236}
2237
2238#[cfg(feature = "secrets")]
2239#[derive(Args, Debug)]
2240pub struct CloudflareSecretsStoreListCmd {}
2241
2242#[cfg(feature = "secrets")]
2243#[derive(Args, Debug)]
2244pub struct CloudflareSecretsStoreGetCmd {
2245 #[arg(long)]
2246 pub store_id: String,
2247}
2248
2249#[cfg(feature = "secrets")]
2250#[derive(Args, Debug)]
2251pub struct CloudflareSecretsStoreCreateCmd {
2252 #[arg(long)]
2253 pub name: String,
2254}
2255
2256#[cfg(feature = "secrets")]
2257#[derive(Args, Debug)]
2258pub struct CloudflareSecretsStoreDeleteCmd {
2259 #[arg(long)]
2260 pub store_id: String,
2261}
2262
2263#[cfg(feature = "secrets")]
2264#[derive(Args, Debug)]
2265pub struct CloudflareSecretsCmd {
2266 #[command(subcommand)]
2267 pub command: CloudflareSecretsSubCommand,
2268}
2269
2270#[cfg(feature = "secrets")]
2271#[derive(Subcommand, Debug)]
2272pub enum CloudflareSecretsSubCommand {
2273 List(CloudflareSecretsListCmd),
2274 Get(CloudflareSecretsGetCmd),
2275 Create(CloudflareSecretsCreateCmd),
2276 Edit(CloudflareSecretsEditCmd),
2277 Delete(CloudflareSecretsDeleteCmd),
2278 #[command(name = "delete-bulk")]
2279 DeleteBulk(CloudflareSecretsBulkDeleteCmd),
2280 Duplicate(CloudflareSecretsDuplicateCmd),
2281}
2282
2283#[cfg(feature = "secrets")]
2284#[derive(Args, Debug)]
2285pub struct CloudflareSecretsListCmd {
2286 #[arg(long)]
2287 pub store_id: String,
2288}
2289
2290#[cfg(feature = "secrets")]
2291#[derive(Args, Debug)]
2292pub struct CloudflareSecretsGetCmd {
2293 #[arg(long)]
2294 pub store_id: String,
2295 #[arg(long)]
2296 pub secret_id: String,
2297}
2298
2299#[cfg(feature = "secrets")]
2300#[derive(Args, Debug)]
2301pub struct CloudflareSecretsCreateCmd {
2302 #[arg(long)]
2303 pub store_id: String,
2304 #[arg(long)]
2305 pub name: String,
2306 #[arg(long)]
2307 pub value: String,
2308 #[arg(long, value_delimiter = ',')]
2309 pub scopes: Vec<String>,
2310 #[arg(long)]
2311 pub comment: Option<String>,
2312}
2313
2314#[cfg(feature = "secrets")]
2315#[derive(Args, Debug)]
2316pub struct CloudflareSecretsEditCmd {
2317 #[arg(long)]
2318 pub store_id: String,
2319 #[arg(long)]
2320 pub secret_id: String,
2321 #[arg(long)]
2322 pub name: Option<String>,
2323 #[arg(long)]
2324 pub value: Option<String>,
2325 #[arg(long, value_delimiter = ',')]
2326 pub scopes: Vec<String>,
2327 #[arg(long)]
2328 pub comment: Option<String>,
2329}
2330
2331#[cfg(feature = "secrets")]
2332#[derive(Args, Debug)]
2333pub struct CloudflareSecretsDeleteCmd {
2334 #[arg(long)]
2335 pub store_id: String,
2336 #[arg(long)]
2337 pub secret_id: String,
2338}
2339
2340#[cfg(feature = "secrets")]
2341#[derive(Args, Debug)]
2342pub struct CloudflareSecretsBulkDeleteCmd {
2343 #[arg(long)]
2344 pub store_id: String,
2345 #[arg(long = "secret-id", required = true)]
2346 pub secret_ids: Vec<String>,
2347}
2348
2349#[cfg(feature = "secrets")]
2350#[derive(Args, Debug)]
2351pub struct CloudflareSecretsDuplicateCmd {
2352 #[arg(long)]
2353 pub store_id: String,
2354 #[arg(long)]
2355 pub secret_id: String,
2356 #[arg(long)]
2357 pub name: String,
2358 #[arg(long, value_delimiter = ',')]
2359 pub scopes: Vec<String>,
2360 #[arg(long)]
2361 pub comment: Option<String>,
2362}
2363
2364#[cfg(feature = "secrets")]
2365#[derive(Args, Debug)]
2366pub struct SecretsQuotaCmd {
2367 #[command(subcommand)]
2368 pub command: SecretsQuotaSubCommand,
2369}
2370
2371#[cfg(feature = "secrets")]
2372#[derive(Subcommand, Debug)]
2373pub enum SecretsQuotaSubCommand {
2374 Get(SecretsQuotaGetCmd),
2375}
2376
2377#[cfg(feature = "secrets")]
2378#[derive(Args, Debug)]
2379pub struct SecretsQuotaGetCmd {}
2380
2381const DNS_HELP_TEMPLATE: &str = crate::cli::help_render::XBP_HELP_TEMPLATE;
2382
2383const DNS_COMMAND_AFTER_HELP: &str = "\
2384Examples:
2385 xbp dns providers
2386 xbp dns zones list --provider cloudflare --account-id acc_123
2387 xbp dns records list --provider cloudflare --zone-id zone_123
2388 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
2389 xbp dns dnssec get --provider cloudflare --zone-id zone_123
2390 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
2391
2392Notes:
2393 Start with `xbp dns providers` to see what is implemented today.
2394 Cloudflare auth comes from `--token`, `CLOUDFLARE_API_TOKEN`, `xbp config cloudflare set-key`, or a linked dashboard account after `xbp login` (`xbp config cloudflare login`).";
2395
2396const DNS_ZONES_AFTER_HELP: &str = "\
2397Examples:
2398 xbp dns zones list --provider cloudflare --account-id acc_123
2399 xbp dns zones get --provider cloudflare --zone-id zone_123
2400 xbp dns zones create --provider cloudflare --name example.com --account-id acc_123 --jump-start
2401 xbp dns zones edit --provider cloudflare --zone-id zone_123 --paused true
2402 xbp dns zones delete --provider cloudflare --zone-id zone_123";
2403
2404const DNS_RECORDS_AFTER_HELP: &str = "\
2405Examples:
2406 xbp dns records list --provider cloudflare --zone-id zone_123
2407 xbp dns records get --provider cloudflare --zone-id zone_123 --record-id rec_123
2408 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
2409 xbp dns records edit --provider cloudflare --zone-id zone_123 --record-id rec_123 --proxied true
2410 xbp dns records import --provider cloudflare --zone-id zone_123 --file zone.txt
2411 xbp dns records export --provider cloudflare --zone-id zone_123 --output zone.txt";
2412
2413const DNS_DNSSEC_AFTER_HELP: &str = "\
2414Examples:
2415 xbp dns dnssec get --provider cloudflare --zone-id zone_123
2416 xbp dns dnssec edit --provider cloudflare --zone-id zone_123 --status active";
2417
2418const DNS_SETTINGS_AFTER_HELP: &str = "\
2419Examples:
2420 xbp dns settings get --provider cloudflare --zone-id zone_123
2421 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
2422 xbp dns settings edit --provider cloudflare --zone-id zone_123 --nameservers-type custom --nameservers-ns-set 2";
2423
2424const DNS_PROVIDERS_AFTER_HELP: &str = "\
2425Examples:
2426 xbp dns providers
2427
2428What this shows:
2429 Implemented providers are wired into `xbp dns` today.
2430 Planned providers are tracked in the CLI surface but not callable yet.";
2431
2432#[derive(Args, Debug)]
2433#[command(
2434 about = "Manage DNS providers, zones, records, DNSSEC, and provider-level settings",
2435 arg_required_else_help = true,
2436 disable_help_subcommand = true,
2437 help_template = DNS_HELP_TEMPLATE,
2438 after_help = DNS_COMMAND_AFTER_HELP
2439)]
2440pub struct DnsCmd {
2441 #[command(subcommand)]
2442 pub command: DnsSubCommand,
2443}
2444
2445#[derive(Subcommand, Debug)]
2446pub enum DnsSubCommand {
2447 #[command(
2448 alias = "ls",
2449 alias = "list",
2450 about = "List supported DNS providers and current implementation status",
2451 after_help = DNS_PROVIDERS_AFTER_HELP
2452 )]
2453 Providers,
2454 #[command(about = "Inspect and manage DNS zones")]
2455 Zones(DnsZonesCmd),
2456 #[command(about = "List, create, edit, import, export, and batch DNS records")]
2457 Records(DnsRecordsCmd),
2458 #[command(about = "Inspect or edit DNSSEC status for a zone")]
2459 Dnssec(DnssecCmd),
2460 #[command(about = "Inspect or edit provider DNS settings for a zone")]
2461 Settings(DnsSettingsCmd),
2462}
2463
2464#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2465pub enum DnsProviderKind {
2466 Cloudflare,
2467 Hetzner,
2468 Vercel,
2469 Custom,
2470}
2471
2472#[derive(Args, Debug)]
2473#[command(
2474 about = "Inspect and manage DNS zones",
2475 arg_required_else_help = true,
2476 help_template = DNS_HELP_TEMPLATE,
2477 after_help = DNS_ZONES_AFTER_HELP
2478)]
2479pub struct DnsZonesCmd {
2480 #[command(subcommand)]
2481 pub command: DnsZonesSubCommand,
2482}
2483
2484#[derive(Subcommand, Debug)]
2485pub enum DnsZonesSubCommand {
2486 #[command(about = "List zones for a provider account")]
2487 List(DnsZoneListCmd),
2488 #[command(about = "Fetch one zone by id")]
2489 Get(DnsZoneGetCmd),
2490 #[command(about = "Create a new zone")]
2491 Create(DnsZoneCreateCmd),
2492 #[command(about = "Edit zone-level properties")]
2493 Edit(DnsZoneEditCmd),
2494 #[command(about = "Delete a zone")]
2495 Delete(DnsZoneDeleteCmd),
2496}
2497
2498#[derive(Args, Debug)]
2499pub struct DnsZoneListCmd {
2500 #[arg(long, value_enum, default_value = "cloudflare")]
2501 pub provider: DnsProviderKind,
2502 #[arg(long)]
2503 pub account_id: Option<String>,
2504 #[arg(long)]
2505 pub account_name: Option<String>,
2506 #[arg(long = "account-name-op")]
2507 pub account_name_op: Option<String>,
2508 #[arg(long)]
2509 pub name: Option<String>,
2510 #[arg(long = "name-op")]
2511 pub name_op: Option<String>,
2512 #[arg(long)]
2513 pub status: Option<String>,
2514 #[arg(long = "type", value_delimiter = ',')]
2515 pub zone_types: Vec<String>,
2516 #[arg(long)]
2517 pub r#match: Option<String>,
2518 #[arg(long)]
2519 pub order: Option<String>,
2520 #[arg(long)]
2521 pub direction: Option<String>,
2522 #[arg(long)]
2523 pub page: Option<u64>,
2524 #[arg(long = "per-page")]
2525 pub per_page: Option<u64>,
2526 #[arg(long)]
2527 pub token: Option<String>,
2528}
2529
2530#[derive(Args, Debug)]
2531pub struct DnsZoneGetCmd {
2532 #[arg(long, value_enum, default_value = "cloudflare")]
2533 pub provider: DnsProviderKind,
2534 #[arg(long)]
2535 pub zone_id: String,
2536 #[arg(long)]
2537 pub token: Option<String>,
2538}
2539
2540#[derive(Args, Debug)]
2541pub struct DnsZoneCreateCmd {
2542 #[arg(long, value_enum, default_value = "cloudflare")]
2543 pub provider: DnsProviderKind,
2544 #[arg(long)]
2545 pub name: String,
2546 #[arg(long)]
2547 pub account_id: Option<String>,
2548 #[arg(long)]
2549 pub jump_start: bool,
2550 #[arg(long = "type")]
2551 pub zone_type: Option<String>,
2552 #[arg(long)]
2553 pub token: Option<String>,
2554}
2555
2556#[derive(Args, Debug)]
2557pub struct DnsZoneEditCmd {
2558 #[arg(long, value_enum, default_value = "cloudflare")]
2559 pub provider: DnsProviderKind,
2560 #[arg(long)]
2561 pub zone_id: String,
2562 #[arg(long)]
2563 pub paused: Option<bool>,
2564 #[arg(long = "type")]
2565 pub zone_type: Option<String>,
2566 #[arg(long = "vanity-name-server")]
2567 pub vanity_name_servers: Vec<String>,
2568 #[arg(long)]
2569 pub token: Option<String>,
2570}
2571
2572#[derive(Args, Debug)]
2573pub struct DnsZoneDeleteCmd {
2574 #[arg(long, value_enum, default_value = "cloudflare")]
2575 pub provider: DnsProviderKind,
2576 #[arg(long)]
2577 pub zone_id: String,
2578 #[arg(long)]
2579 pub token: Option<String>,
2580}
2581
2582#[derive(Args, Debug)]
2583#[command(
2584 about = "List, create, edit, import, export, and batch DNS records",
2585 arg_required_else_help = true,
2586 help_template = DNS_HELP_TEMPLATE,
2587 after_help = DNS_RECORDS_AFTER_HELP
2588)]
2589pub struct DnsRecordsCmd {
2590 #[command(subcommand)]
2591 pub command: DnsRecordsSubCommand,
2592}
2593
2594#[derive(Subcommand, Debug)]
2595pub enum DnsRecordsSubCommand {
2596 #[command(about = "List records in a zone")]
2597 List(DnsRecordListCmd),
2598 #[command(about = "Fetch one record by id")]
2599 Get(DnsRecordGetCmd),
2600 #[command(about = "Create a new DNS record")]
2601 Create(DnsRecordCreateCmd),
2602 #[command(about = "Replace a record by id with a full payload")]
2603 Replace(DnsRecordReplaceCmd),
2604 #[command(about = "Patch selected fields on a record")]
2605 Edit(DnsRecordEditCmd),
2606 #[command(about = "Delete a record")]
2607 Delete(DnsRecordDeleteCmd),
2608 #[command(about = "Apply a Cloudflare batch record payload from JSON")]
2609 Batch(DnsRecordBatchCmd),
2610 #[command(about = "Import a BIND-style zone file into a zone")]
2611 Import(DnsRecordImportCmd),
2612 #[command(about = "Export a zone as a BIND-style zone file")]
2613 Export(DnsRecordExportCmd),
2614}
2615
2616#[derive(Args, Debug)]
2617pub struct DnsRecordListCmd {
2618 #[arg(long, value_enum, default_value = "cloudflare")]
2619 pub provider: DnsProviderKind,
2620 #[arg(long)]
2621 pub zone_id: String,
2622 #[arg(long = "type")]
2623 pub record_type: Option<String>,
2624 #[arg(long)]
2625 pub name: Option<String>,
2626 #[arg(long)]
2627 pub page: Option<u64>,
2628 #[arg(long = "per-page")]
2629 pub per_page: Option<u64>,
2630 #[arg(long)]
2631 pub token: Option<String>,
2632}
2633
2634#[derive(Args, Debug)]
2635pub struct DnsRecordGetCmd {
2636 #[arg(long, value_enum, default_value = "cloudflare")]
2637 pub provider: DnsProviderKind,
2638 #[arg(long)]
2639 pub zone_id: String,
2640 #[arg(long)]
2641 pub record_id: String,
2642 #[arg(long)]
2643 pub token: Option<String>,
2644}
2645
2646#[derive(Args, Debug)]
2647pub struct DnsRecordCreateCmd {
2648 #[arg(long, value_enum, default_value = "cloudflare")]
2649 pub provider: DnsProviderKind,
2650 #[arg(long)]
2651 pub zone_id: String,
2652 #[arg(long = "type")]
2653 pub record_type: String,
2654 #[arg(long)]
2655 pub name: String,
2656 #[arg(long)]
2657 pub content: String,
2658 #[arg(long)]
2659 pub ttl: Option<u32>,
2660 #[arg(long)]
2661 pub proxied: Option<bool>,
2662 #[arg(long)]
2663 pub priority: Option<u32>,
2664 #[arg(long)]
2665 pub comment: Option<String>,
2666 #[arg(long = "tag")]
2667 pub tags: Vec<String>,
2668 #[arg(long = "data-json")]
2669 pub data_json: Option<String>,
2670 #[arg(long = "settings-json")]
2671 pub settings_json: Option<String>,
2672 #[arg(long)]
2673 pub token: Option<String>,
2674}
2675
2676#[derive(Args, Debug)]
2677pub struct DnsRecordReplaceCmd {
2678 #[command(flatten)]
2679 pub common: DnsRecordCreateCmd,
2680 #[arg(long)]
2681 pub record_id: String,
2682}
2683
2684#[derive(Args, Debug)]
2685pub struct DnsRecordEditCmd {
2686 #[arg(long, value_enum, default_value = "cloudflare")]
2687 pub provider: DnsProviderKind,
2688 #[arg(long)]
2689 pub zone_id: String,
2690 #[arg(long)]
2691 pub record_id: String,
2692 #[arg(long = "type")]
2693 pub record_type: Option<String>,
2694 #[arg(long)]
2695 pub name: Option<String>,
2696 #[arg(long)]
2697 pub content: Option<String>,
2698 #[arg(long)]
2699 pub ttl: Option<u32>,
2700 #[arg(long)]
2701 pub proxied: Option<bool>,
2702 #[arg(long)]
2703 pub priority: Option<u32>,
2704 #[arg(long)]
2705 pub comment: Option<String>,
2706 #[arg(long = "tag")]
2707 pub tags: Vec<String>,
2708 #[arg(long = "data-json")]
2709 pub data_json: Option<String>,
2710 #[arg(long = "settings-json")]
2711 pub settings_json: Option<String>,
2712 #[arg(long)]
2713 pub token: Option<String>,
2714}
2715
2716#[derive(Args, Debug)]
2717pub struct DnsRecordDeleteCmd {
2718 #[arg(long, value_enum, default_value = "cloudflare")]
2719 pub provider: DnsProviderKind,
2720 #[arg(long)]
2721 pub zone_id: String,
2722 #[arg(long)]
2723 pub record_id: String,
2724 #[arg(long)]
2725 pub token: Option<String>,
2726}
2727
2728#[derive(Args, Debug)]
2729pub struct DnsRecordBatchCmd {
2730 #[arg(long, value_enum, default_value = "cloudflare")]
2731 pub provider: DnsProviderKind,
2732 #[arg(long)]
2733 pub zone_id: String,
2734 #[arg(long)]
2735 pub input: PathBuf,
2736 #[arg(long)]
2737 pub token: Option<String>,
2738}
2739
2740#[derive(Args, Debug)]
2741pub struct DnsRecordImportCmd {
2742 #[arg(long, value_enum, default_value = "cloudflare")]
2743 pub provider: DnsProviderKind,
2744 #[arg(long)]
2745 pub zone_id: String,
2746 #[arg(long)]
2747 pub file: PathBuf,
2748 #[arg(long)]
2749 pub token: Option<String>,
2750}
2751
2752#[derive(Args, Debug)]
2753pub struct DnsRecordExportCmd {
2754 #[arg(long, value_enum, default_value = "cloudflare")]
2755 pub provider: DnsProviderKind,
2756 #[arg(long)]
2757 pub zone_id: String,
2758 #[arg(long)]
2759 pub output: Option<PathBuf>,
2760 #[arg(long)]
2761 pub token: Option<String>,
2762}
2763
2764#[derive(Args, Debug)]
2765#[command(
2766 about = "Inspect or edit DNSSEC status for a zone",
2767 arg_required_else_help = true,
2768 help_template = DNS_HELP_TEMPLATE,
2769 after_help = DNS_DNSSEC_AFTER_HELP
2770)]
2771pub struct DnssecCmd {
2772 #[command(subcommand)]
2773 pub command: DnssecSubCommand,
2774}
2775
2776#[derive(Subcommand, Debug)]
2777pub enum DnssecSubCommand {
2778 #[command(about = "Fetch DNSSEC state for a zone")]
2779 Get(DnssecGetCmd),
2780 #[command(about = "Edit DNSSEC-related flags for a zone")]
2781 Edit(DnssecEditCmd),
2782}
2783
2784#[derive(Args, Debug)]
2785pub struct DnssecGetCmd {
2786 #[arg(long, value_enum, default_value = "cloudflare")]
2787 pub provider: DnsProviderKind,
2788 #[arg(long)]
2789 pub zone_id: String,
2790 #[arg(long)]
2791 pub token: Option<String>,
2792}
2793
2794#[derive(Args, Debug)]
2795pub struct DnssecEditCmd {
2796 #[arg(long, value_enum, default_value = "cloudflare")]
2797 pub provider: DnsProviderKind,
2798 #[arg(long)]
2799 pub zone_id: String,
2800 #[arg(long)]
2801 pub status: Option<String>,
2802 #[arg(long = "dnssec-multi-signer")]
2803 pub dnssec_multi_signer: Option<bool>,
2804 #[arg(long = "dnssec-presigned")]
2805 pub dnssec_presigned: Option<bool>,
2806 #[arg(long = "dnssec-use-nsec3")]
2807 pub dnssec_use_nsec3: Option<bool>,
2808 #[arg(long)]
2809 pub token: Option<String>,
2810}
2811
2812#[derive(Args, Debug)]
2813#[command(
2814 about = "Inspect or edit provider DNS settings for a zone",
2815 arg_required_else_help = true,
2816 help_template = DNS_HELP_TEMPLATE,
2817 after_help = DNS_SETTINGS_AFTER_HELP
2818)]
2819pub struct DnsSettingsCmd {
2820 #[command(subcommand)]
2821 pub command: DnsSettingsSubCommand,
2822}
2823
2824#[derive(Subcommand, Debug)]
2825pub enum DnsSettingsSubCommand {
2826 #[command(about = "Fetch provider DNS settings for a zone")]
2827 Get(DnsSettingsGetCmd),
2828 #[command(about = "Edit provider DNS settings for a zone")]
2829 Edit(DnsSettingsEditCmd),
2830}
2831
2832#[derive(Args, Debug)]
2833pub struct DnsSettingsGetCmd {
2834 #[arg(long, value_enum, default_value = "cloudflare")]
2835 pub provider: DnsProviderKind,
2836 #[arg(long)]
2837 pub zone_id: String,
2838 #[arg(long)]
2839 pub token: Option<String>,
2840}
2841
2842#[derive(Args, Debug)]
2843pub struct DnsSettingsEditCmd {
2844 #[arg(long, value_enum, default_value = "cloudflare")]
2845 pub provider: DnsProviderKind,
2846 #[arg(long)]
2847 pub zone_id: String,
2848 #[arg(long)]
2849 pub flatten_all_cnames: Option<bool>,
2850 #[arg(long)]
2851 pub foundation_dns: Option<bool>,
2852 #[arg(long)]
2853 pub multi_provider: Option<bool>,
2854 #[arg(long)]
2855 pub ns_ttl: Option<u32>,
2856 #[arg(long)]
2857 pub secondary_overrides: Option<bool>,
2858 #[arg(long)]
2859 pub zone_mode: Option<String>,
2860 #[arg(long = "reference-zone-id")]
2861 pub reference_zone_id: Option<String>,
2862 #[arg(long = "nameservers-type")]
2863 pub nameservers_type: Option<String>,
2864 #[arg(long = "nameservers-ns-set")]
2865 pub nameservers_ns_set: Option<u32>,
2866 #[arg(long = "soa-json")]
2867 pub soa_json: Option<String>,
2868 #[arg(long)]
2869 pub token: Option<String>,
2870}
2871
2872#[derive(Args, Debug)]
2873#[command(
2874 arg_required_else_help = true,
2875 disable_help_subcommand = true,
2876 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2877 after_help = crate::cli::help_render::DOMAINS_AFTER_HELP
2878)]
2879pub struct DomainsCmd {
2880 #[arg(long, value_enum, default_value = "cloudflare")]
2881 pub provider: DomainsProviderKind,
2882 #[arg(long)]
2883 pub account_id: Option<String>,
2884 #[arg(long)]
2885 pub token: Option<String>,
2886 #[command(subcommand)]
2887 pub command: DomainsSubCommand,
2888}
2889
2890#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2891pub enum DomainsProviderKind {
2892 Cloudflare,
2893}
2894
2895#[derive(Subcommand, Debug)]
2896pub enum DomainsSubCommand {
2897 Search(DomainsSearchCmd),
2898 Check(DomainsCheckCmd),
2899 List(DomainsListCmd),
2900}
2901
2902#[derive(Args, Debug)]
2903pub struct DomainsSearchCmd {
2904 #[arg(long)]
2905 pub query: String,
2906 #[arg(long = "extension")]
2907 pub extensions: Vec<String>,
2908 #[arg(long)]
2909 pub limit: Option<usize>,
2910}
2911
2912#[derive(Args, Debug)]
2913pub struct DomainsCheckCmd {
2914 #[arg(long = "domain", required = true)]
2915 pub domains: Vec<String>,
2916}
2917
2918#[derive(Args, Debug)]
2919pub struct DomainsListCmd {}
2920
2921#[derive(Args, Debug)]
2922pub struct GenerateSystemdCmd {
2923 #[arg(
2924 long,
2925 default_value = "/etc/systemd/system",
2926 help = "Directory where the systemd units are written"
2927 )]
2928 pub output_dir: PathBuf,
2929 #[arg(long, help = "Only generate the unit for this service name")]
2930 pub service: Option<String>,
2931 #[arg(
2932 long,
2933 default_value_t = true,
2934 help = "Also generate the xbp-api systemd unit alongside project/services"
2935 )]
2936 pub api: bool,
2937}
2938
2939#[derive(Args, Debug)]
2940#[command(
2941 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2942 after_help = crate::cli::help_render::DONE_AFTER_HELP
2943)]
2944pub struct DoneCmd {
2945 #[arg(long, help = "Root directory under which to discover git repos")]
2946 pub root: Option<std::path::PathBuf>,
2947 #[arg(
2948 long,
2949 default_value = "24 hours ago",
2950 help = "Git --since value (e.g. '7 days ago')"
2951 )]
2952 pub since: String,
2953 #[arg(short, long, help = "Output Markdown file path")]
2954 pub output: Option<std::path::PathBuf>,
2955 #[arg(long, help = "Skip AI summarization (OpenRouter)")]
2956 pub no_ai: bool,
2957 #[arg(short, long, help = "Discover repos recursively")]
2958 pub recursive: bool,
2959 #[arg(long, help = "Exclude repo by name (repeatable)")]
2960 pub exclude: Vec<String>,
2961}
2962
2963#[derive(Args, Debug)]
2964pub struct FixProcessMonitorJsonCmd {
2965 #[arg(help = "Path to a Cursor process-monitor JSON export")]
2966 pub path: std::path::PathBuf,
2967 #[arg(
2968 long,
2969 help = "Check whether the file needs repair without writing changes"
2970 )]
2971 pub check: bool,
2972 #[arg(
2973 long,
2974 help = "Print repaired JSON to stdout instead of overwriting the file"
2975 )]
2976 pub stdout: bool,
2977}
2978
2979#[derive(Args, Debug)]
2980pub struct CursorCmd {
2981 #[command(subcommand)]
2982 pub command: CursorSubCommand,
2983}
2984
2985#[derive(Subcommand, Debug)]
2986pub enum CursorSubCommand {
2987 #[command(about = "Upload local Cursor file history to the XBP dashboard")]
2988 Ingest {
2989 #[arg(
2990 long,
2991 help = "Scan local Cursor history without uploading to the dashboard"
2992 )]
2993 dry_run: bool,
2994 },
2995}
2996
2997#[cfg(feature = "nordvpn")]
2998#[derive(Args, Debug)]
2999pub struct NordvpnCmd {
3000 #[arg(
3001 trailing_var_arg = true,
3002 allow_hyphen_values = true,
3003 help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
3004 )]
3005 pub args: Vec<String>,
3006}
3007
3008#[cfg(feature = "kubernetes")]
3009#[derive(Args, Debug)]
3010pub struct KubernetesCmd {
3011 #[command(subcommand)]
3012 pub command: KubernetesSubCommand,
3013}
3014
3015#[cfg(feature = "kubernetes")]
3016#[derive(Args, Debug)]
3017pub struct KubernetesAddonCmd {
3018 #[command(subcommand)]
3019 pub command: KubernetesAddonSubCommand,
3020}
3021
3022#[cfg(feature = "kubernetes")]
3023#[derive(Subcommand, Debug)]
3024pub enum KubernetesAddonSubCommand {
3025 List,
3027 Enable {
3029 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
3030 name: String,
3031 },
3032 Disable {
3034 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
3035 name: String,
3036 },
3037}
3038
3039#[cfg(feature = "kubernetes")]
3040#[derive(Subcommand, Debug)]
3041pub enum KubernetesSubCommand {
3042 Check {
3044 #[arg(long, help = "Kubeconfig context to target")]
3045 context: Option<String>,
3046 #[arg(
3047 long,
3048 default_value = "default",
3049 help = "Namespace to probe for workload readiness"
3050 )]
3051 namespace: String,
3052 #[arg(long, help = "Skip live cluster calls (tooling check only)")]
3053 offline: bool,
3054 },
3055 Generate {
3057 #[arg(long, help = "Logical app name (used for resource names)")]
3058 name: String,
3059 #[arg(long, help = "Container image reference")]
3060 image: String,
3061 #[arg(long, default_value_t = 80, help = "Container port for the service")]
3062 port: u16,
3063 #[arg(long, default_value_t = 1, help = "Replica count")]
3064 replicas: u16,
3065 #[arg(
3066 long,
3067 default_value = "default",
3068 help = "Namespace for generated resources"
3069 )]
3070 namespace: String,
3071 #[arg(
3072 long,
3073 default_value = "k8s/xbp-manifest.yaml",
3074 help = "Path to write the manifest bundle"
3075 )]
3076 output: String,
3077 #[arg(long, help = "Optional ingress host (creates Ingress when set)")]
3078 host: Option<String>,
3079 },
3080 Apply {
3082 #[arg(long, help = "Path to manifest file")]
3083 file: String,
3084 #[arg(long, help = "Override kube context")]
3085 context: Option<String>,
3086 #[arg(long, help = "Override namespace")]
3087 namespace: Option<String>,
3088 #[arg(long, help = "Use --dry-run=server")]
3089 dry_run: bool,
3090 },
3091 Status {
3093 #[arg(long, default_value = "default", help = "Namespace to summarize")]
3094 namespace: String,
3095 #[arg(long, help = "Override kube context")]
3096 context: Option<String>,
3097 },
3098 Addons(KubernetesAddonCmd),
3100 DashboardToken {
3102 #[arg(
3103 long,
3104 default_value = "kube-system",
3105 help = "Namespace containing the dashboard token secret"
3106 )]
3107 namespace: String,
3108 #[arg(
3109 long,
3110 default_value = "microk8s-dashboard-token",
3111 help = "Secret name containing the dashboard login token"
3112 )]
3113 secret: String,
3114 #[arg(long, help = "Override kube context")]
3115 context: Option<String>,
3116 },
3117 ObservabilityCreds {
3119 #[arg(
3120 long,
3121 default_value = "observability",
3122 help = "Namespace containing Grafana secret"
3123 )]
3124 namespace: String,
3125 #[arg(
3126 long,
3127 default_value = "kube-prom-stack-grafana",
3128 help = "Grafana secret name"
3129 )]
3130 secret: String,
3131 #[arg(long, help = "Override kube context")]
3132 context: Option<String>,
3133 },
3134 Issuer {
3136 #[arg(
3137 long,
3138 help = "Email used for Let's Encrypt account registration (required)"
3139 )]
3140 email: String,
3141 #[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
3142 name: String,
3143 #[arg(
3144 long,
3145 default_value = "default",
3146 help = "Namespace for the Issuer resource"
3147 )]
3148 namespace: String,
3149 #[arg(
3150 long,
3151 default_value = "https://acme-v02.api.letsencrypt.org/directory",
3152 help = "ACME server URL (production by default)"
3153 )]
3154 server: String,
3155 #[arg(
3156 long,
3157 default_value = "letsencrypt-account-key",
3158 help = "Secret used to store the ACME account private key"
3159 )]
3160 private_key_secret: String,
3161 #[arg(
3162 long,
3163 default_value = "nginx",
3164 help = "Ingress class name used for HTTP01 solving"
3165 )]
3166 ingress_class_name: String,
3167 #[arg(long, help = "Override kube context")]
3168 context: Option<String>,
3169 #[arg(long, help = "Use --dry-run=server")]
3170 dry_run: bool,
3171 },
3172}
3173
3174#[cfg(test)]
3175mod tests {
3176 use super::{
3177 Cli, CloudflareConfigAction, CloudflaredSubCommand, Commands, DnsProviderKind,
3178 DnsSubCommand, DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand,
3179 GenerateSubCommand, LinearConfigAction, NetworkFloatingIpSubCommand,
3180 NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand, NetworkSubCommand, SshCmd,
3181 };
3182 #[cfg(feature = "secrets")]
3183 use super::{
3184 CloudflareSecretsSubCommand, SecretsProviderKind, SecretsStoresSubCommand,
3185 SecretsSubCommand,
3186 };
3187 use clap::Parser;
3188 use std::path::PathBuf;
3189
3190 #[test]
3191 fn parses_network_floating_ip_add() {
3192 let cli = Cli::parse_from([
3193 "xbp",
3194 "network",
3195 "floating-ip",
3196 "add",
3197 "--ip",
3198 "1.2.3.4",
3199 "--apply",
3200 ]);
3201
3202 match cli.command {
3203 Some(Commands::Network(network)) => match network.command {
3204 NetworkSubCommand::FloatingIp(fip) => match fip.command {
3205 NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
3206 assert_eq!(ip, "1.2.3.4");
3207 assert!(apply);
3208 }
3209 _ => panic!("expected add subcommand"),
3210 },
3211 _ => panic!("expected floating-ip subcommand"),
3212 },
3213 _ => panic!("expected network command"),
3214 }
3215 }
3216
3217 #[test]
3218 fn parses_generate_config_update() {
3219 let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
3220
3221 match cli.command {
3222 Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
3223 GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
3224 _ => panic!("expected generate config command"),
3225 },
3226 _ => panic!("expected generate command"),
3227 }
3228 }
3229
3230 #[test]
3231 fn parses_commit_command_with_dry_run() {
3232 let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
3233
3234 match cli.command {
3235 Some(Commands::Commit(commit_cmd)) => {
3236 assert!(commit_cmd.dry_run);
3237 assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
3238 assert_eq!(commit_cmd.model, None);
3239 }
3240 _ => panic!("expected commit command"),
3241 }
3242 }
3243
3244 #[test]
3245 fn parses_linear_select_initiative_config_command() {
3246 let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
3247
3248 match cli.command {
3249 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
3250 Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
3251 assert!(matches!(
3252 linear_cmd.action,
3253 LinearConfigAction::SelectInitiative
3254 ));
3255 }
3256 _ => panic!("expected linear config provider"),
3257 },
3258 _ => panic!("expected config command"),
3259 }
3260 }
3261
3262 #[test]
3263 fn parses_ssh_command_with_cloudflared_and_key_auth() {
3264 let cli = Cli::parse_from([
3265 "xbp",
3266 "ssh",
3267 "--host",
3268 "ssh.internal",
3269 "--username",
3270 "deploy",
3271 "--private-key",
3272 "C:/Users/floris/.ssh/id_ed25519",
3273 "--cloudflared-hostname",
3274 "bastion.example.com",
3275 "--command",
3276 "htop",
3277 ]);
3278
3279 let Some(Commands::Ssh(SshCmd {
3280 ssh_host,
3281 ssh_username,
3282 private_key,
3283 cloudflared_hostname,
3284 command,
3285 ..
3286 })) = cli.command
3287 else {
3288 panic!("expected shell command");
3289 };
3290
3291 assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
3292 assert_eq!(ssh_username.as_deref(), Some("deploy"));
3293 assert_eq!(
3294 private_key,
3295 Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
3296 );
3297 assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
3298 assert_eq!(command.as_deref(), Some("htop"));
3299 }
3300
3301 #[test]
3302 fn parses_cloudflared_tcp_command() {
3303 let cli = Cli::parse_from([
3304 "xbp",
3305 "cloudflared",
3306 "tcp",
3307 "--hostname",
3308 "bastion.example.com",
3309 "--listener",
3310 "127.0.0.1:2222",
3311 ]);
3312
3313 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
3314 panic!("expected cloudflared command");
3315 };
3316
3317 match cloudflared_cmd.command {
3318 CloudflaredSubCommand::Tcp(tcp_cmd) => {
3319 assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
3320 assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
3321 }
3322 }
3323 }
3324
3325 #[test]
3326 fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
3327 let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
3328
3329 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
3330 panic!("expected cloudflared command");
3331 };
3332
3333 match cloudflared_cmd.command {
3334 CloudflaredSubCommand::Tcp(tcp_cmd) => {
3335 assert_eq!(tcp_cmd.hostname, None);
3336 assert_eq!(tcp_cmd.listener, None);
3337 }
3338 }
3339 }
3340
3341 #[test]
3342 fn parses_version_workspace_publish_run_command() {
3343 let cli = Cli::parse_from([
3344 "xbp",
3345 "version",
3346 "workspace",
3347 "publish",
3348 "run",
3349 "--repo",
3350 "C:/Users/floris/Documents/GitHub/athena",
3351 "--dry-run",
3352 "--from",
3353 "athena-s3",
3354 ]);
3355
3356 let Some(Commands::Version(version_cmd)) = cli.command else {
3357 panic!("expected version command");
3358 };
3359
3360 match version_cmd.command {
3361 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
3362 match workspace_cmd.command {
3363 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
3364 match publish_cmd.command {
3365 super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
3366 assert_eq!(
3367 run_cmd.target.repo,
3368 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
3369 );
3370 assert!(!run_cmd.target.json);
3371 assert!(run_cmd.dry_run);
3372 assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
3373 }
3374 _ => panic!("expected publish run"),
3375 }
3376 }
3377 _ => panic!("expected workspace publish"),
3378 }
3379 }
3380 _ => panic!("expected version workspace command"),
3381 }
3382 }
3383
3384 #[test]
3385 fn parses_version_workspace_publish_plan_with_only_and_include_prereqs() {
3386 let cli = Cli::parse_from([
3387 "xbp",
3388 "version",
3389 "workspace",
3390 "publish",
3391 "plan",
3392 "--repo",
3393 "C:/Users/floris/Documents/GitHub/athena-auth",
3394 "--only",
3395 "athena-auth",
3396 "--include-prereqs",
3397 ]);
3398
3399 let Some(Commands::Version(version_cmd)) = cli.command else {
3400 panic!("expected version command");
3401 };
3402
3403 match version_cmd.command {
3404 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
3405 match workspace_cmd.command {
3406 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
3407 match publish_cmd.command {
3408 super::VersionWorkspacePublishSubCommand::Plan(plan_cmd) => {
3409 assert_eq!(
3410 plan_cmd.target.repo,
3411 Some(PathBuf::from(
3412 "C:/Users/floris/Documents/GitHub/athena-auth"
3413 ))
3414 );
3415 assert_eq!(plan_cmd.only.as_deref(), Some("athena-auth"));
3416 assert!(plan_cmd.include_prereqs);
3417 }
3418 _ => panic!("expected publish plan"),
3419 }
3420 }
3421 _ => panic!("expected workspace publish"),
3422 }
3423 }
3424 _ => panic!("expected version workspace command"),
3425 }
3426 }
3427
3428 #[test]
3429 fn parses_commit_alias_with_push_flag() {
3430 let cli = Cli::parse_from(["xbp", "c", "-p"]);
3431
3432 let Some(Commands::Commit(commit_cmd)) = cli.command else {
3433 panic!("expected commit command");
3434 };
3435
3436 assert!(commit_cmd.push);
3437 assert!(!commit_cmd.dry_run);
3438 }
3439
3440 #[test]
3441 fn parses_version_alias_release_alias() {
3442 let cli = Cli::parse_from(["xbp", "v", "r", "--draft", "--publish", "--force"]);
3443
3444 let Some(Commands::Version(version_cmd)) = cli.command else {
3445 panic!("expected version command");
3446 };
3447
3448 let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
3449 panic!("expected release subcommand");
3450 };
3451
3452 assert!(release_cmd.draft);
3453 assert!(release_cmd.publish);
3454 assert!(release_cmd.force);
3455 }
3456
3457 #[test]
3458 fn parses_publish_command_target_filter() {
3459 let cli = Cli::parse_from([
3460 "xbp",
3461 "publish",
3462 "--allow-dirty",
3463 "--force",
3464 "--include-prereqs",
3465 "--target",
3466 "npm",
3467 "--manifest-path",
3468 "apps/web/package.json",
3469 ]);
3470
3471 let Some(Commands::Publish(publish_cmd)) = cli.command else {
3472 panic!("expected publish command");
3473 };
3474
3475 assert!(publish_cmd.allow_dirty);
3476 assert!(publish_cmd.force);
3477 assert!(publish_cmd.include_prereqs);
3478 assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
3479 assert_eq!(
3480 publish_cmd.manifest_path,
3481 Some(PathBuf::from("apps/web/package.json"))
3482 );
3483 }
3484
3485 #[test]
3486 fn parses_npm_setup_release_config_command() {
3487 let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
3488
3489 let Some(Commands::Config(config_cmd)) = cli.command else {
3490 panic!("expected config command");
3491 };
3492 let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
3493 panic!("expected npm config command");
3494 };
3495
3496 assert!(matches!(
3497 registry_cmd.action,
3498 super::RegistryConfigAction::SetupRelease
3499 ));
3500 }
3501
3502 #[test]
3503 fn parses_crates_login_config_command() {
3504 let cli = Cli::parse_from(["xbp", "config", "crates", "login"]);
3505
3506 let Some(Commands::Config(config_cmd)) = cli.command else {
3507 panic!("expected config command");
3508 };
3509 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
3510 panic!("expected crates config command");
3511 };
3512
3513 assert!(matches!(
3514 crates_cmd.action,
3515 super::CratesConfigAction::Login { .. }
3516 ));
3517 }
3518
3519 #[test]
3520 fn parses_crates_logout_config_command() {
3521 let cli = Cli::parse_from(["xbp", "config", "crates", "logout"]);
3522
3523 let Some(Commands::Config(config_cmd)) = cli.command else {
3524 panic!("expected config command");
3525 };
3526 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
3527 panic!("expected crates config command");
3528 };
3529
3530 assert!(matches!(
3531 crates_cmd.action,
3532 super::CratesConfigAction::Logout
3533 ));
3534 }
3535
3536 #[test]
3537 fn parses_shell_alias_as_ssh_command() {
3538 let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
3539
3540 let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
3541 panic!("expected ssh command through shell alias");
3542 };
3543
3544 assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
3545 }
3546
3547 #[test]
3548 fn parses_api_request_command() {
3549 let cli = Cli::parse_from([
3550 "xbp",
3551 "api",
3552 "request",
3553 "/api/registry/installers/python-pip",
3554 "--web",
3555 "--method",
3556 "GET",
3557 "--header",
3558 "accept: application/json",
3559 ]);
3560
3561 let Some(Commands::Api(api_cmd)) = cli.command else {
3562 panic!("expected api command");
3563 };
3564
3565 match api_cmd.command {
3566 super::ApiSubCommand::Request(request_cmd) => {
3567 assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
3568 assert!(request_cmd.target.web);
3569 assert_eq!(request_cmd.method.as_deref(), Some("GET"));
3570 assert_eq!(
3571 request_cmd.target.header,
3572 vec!["accept: application/json".to_string()]
3573 );
3574 }
3575 _ => panic!("expected api request subcommand"),
3576 }
3577 }
3578
3579 #[test]
3580 fn parses_api_projects_list_command() {
3581 let cli = Cli::parse_from([
3582 "xbp",
3583 "api",
3584 "projects",
3585 "list",
3586 "--organization-id",
3587 "org_123",
3588 ]);
3589
3590 let Some(Commands::Api(api_cmd)) = cli.command else {
3591 panic!("expected api command");
3592 };
3593
3594 match api_cmd.command {
3595 super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
3596 super::ApiProjectsSubCommand::List(list_cmd) => {
3597 assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
3598 }
3599 _ => panic!("expected projects list subcommand"),
3600 },
3601 _ => panic!("expected projects subcommand"),
3602 }
3603 }
3604
3605 #[test]
3606 fn parses_api_routes_create_command() {
3607 let cli = Cli::parse_from([
3608 "xbp",
3609 "api",
3610 "routes",
3611 "create",
3612 "--domain",
3613 "demo.local",
3614 "--target",
3615 "http://127.0.0.1:3000",
3616 "--weighted-target",
3617 "http://127.0.0.1:3001=3",
3618 "--base-url",
3619 "http://127.0.0.1:8080",
3620 ]);
3621
3622 let Some(Commands::Api(api_cmd)) = cli.command else {
3623 panic!("expected api command");
3624 };
3625
3626 match api_cmd.command {
3627 super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
3628 super::ApiRoutesSubCommand::Create(create_cmd) => {
3629 assert_eq!(create_cmd.domain, "demo.local");
3630 assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
3631 assert_eq!(
3632 create_cmd.weighted_target,
3633 vec!["http://127.0.0.1:3001=3".to_string()]
3634 );
3635 assert_eq!(
3636 create_cmd.target_options.base_url.as_deref(),
3637 Some("http://127.0.0.1:8080")
3638 );
3639 }
3640 _ => panic!("expected routes create subcommand"),
3641 },
3642 _ => panic!("expected routes subcommand"),
3643 }
3644 }
3645
3646 #[test]
3647 fn parses_hetzner_vswitch_setup_command() {
3648 let cli = Cli::parse_from([
3649 "xbp",
3650 "network",
3651 "hetzner",
3652 "vswitch",
3653 "setup",
3654 "--ip",
3655 "10.0.3.2",
3656 "--vlan-id",
3657 "4000",
3658 "--interface",
3659 "enp0s31f6",
3660 "--apply",
3661 ]);
3662
3663 let Some(Commands::Network(network_cmd)) = cli.command else {
3664 panic!("expected network command");
3665 };
3666
3667 match network_cmd.command {
3668 NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
3669 NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
3670 NetworkHetznerVswitchSubCommand::Setup {
3671 ip,
3672 cidr,
3673 interface,
3674 vlan_id,
3675 apply,
3676 ..
3677 } => {
3678 assert_eq!(ip, "10.0.3.2");
3679 assert_eq!(cidr, 24);
3680 assert_eq!(interface.as_deref(), Some("enp0s31f6"));
3681 assert_eq!(vlan_id, 4000);
3682 assert!(apply);
3683 }
3684 },
3685 },
3686 _ => panic!("expected hetzner subcommand"),
3687 }
3688 }
3689
3690 #[cfg(feature = "secrets")]
3691 #[test]
3692 fn parses_secrets_diag_command() {
3693 let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
3694
3695 match cli.command {
3696 Some(Commands::Secrets(secrets_cmd)) => {
3697 assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
3698 assert_eq!(secrets_cmd.environment, "xbp-dev");
3699 }
3700 _ => panic!("expected secrets command"),
3701 }
3702 }
3703
3704 #[cfg(feature = "secrets")]
3705 #[test]
3706 fn parses_secrets_environment_override() {
3707 let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
3708
3709 match cli.command {
3710 Some(Commands::Secrets(secrets_cmd)) => {
3711 assert_eq!(secrets_cmd.environment, "xbp-prod");
3712 assert!(matches!(
3713 secrets_cmd.command,
3714 Some(SecretsSubCommand::Push(_))
3715 ));
3716 }
3717 _ => panic!("expected secrets command"),
3718 }
3719 }
3720
3721 #[test]
3722 fn parses_version_discover_command() {
3723 let cli = Cli::parse_from(["xbp", "version", "discover", "--dry-run"]);
3724
3725 match cli.command {
3726 Some(Commands::Version(version_cmd)) => match version_cmd.command {
3727 Some(super::VersionSubCommand::Discover(discover_cmd)) => {
3728 assert!(discover_cmd.dry_run);
3729 assert!(!discover_cmd.no_register);
3730 }
3731 _ => panic!("expected version discover subcommand"),
3732 },
3733 _ => panic!("expected version command"),
3734 }
3735 }
3736
3737 #[cfg(feature = "secrets")]
3738 #[test]
3739 fn parses_secrets_providers_command() {
3740 let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
3741
3742 match cli.command {
3743 Some(Commands::Secrets(secrets_cmd)) => {
3744 assert!(matches!(
3745 secrets_cmd.command,
3746 Some(SecretsSubCommand::Providers)
3747 ));
3748 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
3749 }
3750 _ => panic!("expected secrets command"),
3751 }
3752 }
3753
3754 #[cfg(feature = "secrets")]
3755 #[test]
3756 fn parses_cloudflare_secret_store_create() {
3757 let cli = Cli::parse_from([
3758 "xbp",
3759 "secrets",
3760 "--provider",
3761 "cloudflare",
3762 "stores",
3763 "create",
3764 "--name",
3765 "prod",
3766 ]);
3767
3768 match cli.command {
3769 Some(Commands::Secrets(secrets_cmd)) => {
3770 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
3771 match secrets_cmd.command {
3772 Some(SecretsSubCommand::Stores(stores_cmd)) => {
3773 assert!(matches!(
3774 stores_cmd.command,
3775 SecretsStoresSubCommand::Create(_)
3776 ));
3777 }
3778 _ => panic!("expected stores subcommand"),
3779 }
3780 }
3781 _ => panic!("expected secrets command"),
3782 }
3783 }
3784
3785 #[cfg(feature = "secrets")]
3786 #[test]
3787 fn parses_cloudflare_secret_duplicate() {
3788 let cli = Cli::parse_from([
3789 "xbp",
3790 "secrets",
3791 "--provider",
3792 "cloudflare",
3793 "secrets",
3794 "duplicate",
3795 "--store-id",
3796 "store_1",
3797 "--secret-id",
3798 "secret_1",
3799 "--name",
3800 "COPY",
3801 ]);
3802
3803 match cli.command {
3804 Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
3805 Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
3806 assert!(matches!(
3807 secrets_cmd.command,
3808 CloudflareSecretsSubCommand::Duplicate(_)
3809 ));
3810 }
3811 _ => panic!("expected cloudflare secrets subcommand"),
3812 },
3813 _ => panic!("expected secrets command"),
3814 }
3815 }
3816
3817 #[test]
3818 fn parses_workers_secret_put_from_stdin_command() {
3819 let cli = Cli::parse_from([
3820 "xbp",
3821 "workers",
3822 "secrets",
3823 "--environment",
3824 "production",
3825 "put",
3826 "--name",
3827 "API_KEY",
3828 "--from-stdin",
3829 ]);
3830
3831 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3832 panic!("expected workers command");
3833 };
3834
3835 match workers_cmd.command {
3836 super::WorkersSubCommand::Secrets(secrets_cmd) => {
3837 assert_eq!(
3838 secrets_cmd.target.environment.as_deref(),
3839 Some("production")
3840 );
3841 match secrets_cmd.command {
3842 super::WorkersSecretsSubCommand::Put(put_cmd) => {
3843 assert_eq!(put_cmd.name, "API_KEY");
3844 assert!(put_cmd.from_stdin);
3845 assert_eq!(put_cmd.value, None);
3846 }
3847 _ => panic!("expected workers secret put"),
3848 }
3849 }
3850 _ => panic!("expected workers secrets command"),
3851 }
3852 }
3853
3854 #[test]
3855 fn parses_workers_d1_migrations_local_command() {
3856 let cli = Cli::parse_from([
3857 "xbp",
3858 "workers",
3859 "d1",
3860 "migrations",
3861 "apply",
3862 "DB",
3863 "--local",
3864 "--environment",
3865 "preview",
3866 ]);
3867
3868 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3869 panic!("expected workers command");
3870 };
3871
3872 match workers_cmd.command {
3873 super::WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
3874 super::WorkersD1SubCommand::Migrations(migrations_cmd) => {
3875 match migrations_cmd.command {
3876 super::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
3877 assert_eq!(apply_cmd.database, "DB");
3878 assert!(apply_cmd.local);
3879 assert!(!apply_cmd.remote);
3880 assert_eq!(apply_cmd.target.environment.as_deref(), Some("preview"));
3881 }
3882 }
3883 }
3884 },
3885 _ => panic!("expected workers d1 command"),
3886 }
3887 }
3888
3889 #[test]
3890 fn parses_workers_deploy_ci_version_upload_command() {
3891 let cli = Cli::parse_from(["xbp", "workers", "deploy", "ci", "--version-upload"]);
3892
3893 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3894 panic!("expected workers command");
3895 };
3896
3897 match workers_cmd.command {
3898 super::WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
3899 super::WorkersDeploySubCommand::Ci(ci_cmd) => {
3900 assert!(ci_cmd.version_upload);
3901 }
3902 _ => panic!("expected workers deploy ci command"),
3903 },
3904 _ => panic!("expected workers deploy command"),
3905 }
3906 }
3907
3908 #[test]
3909 fn parses_workers_list_alias_command() {
3910 let cli = Cli::parse_from(["xbp", "workers", "ls", "--all"]);
3911
3912 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3913 panic!("expected workers command");
3914 };
3915
3916 match workers_cmd.command {
3917 super::WorkersSubCommand::List(list_cmd) => {
3918 assert!(list_cmd.all);
3919 assert!(!list_cmd.json);
3920 }
3921 _ => panic!("expected workers list command"),
3922 }
3923 }
3924
3925 #[test]
3926 fn parses_workers_logs_follow_and_build_flags() {
3927 let cli = Cli::parse_from([
3928 "xbp",
3929 "workers",
3930 "logs",
3931 "-f",
3932 "--build",
3933 "--failed",
3934 "xbp-production",
3935 ]);
3936
3937 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3938 panic!("expected workers command");
3939 };
3940
3941 match workers_cmd.command {
3942 super::WorkersSubCommand::Logs(logs_cmd) => {
3943 assert!(logs_cmd.follow);
3944 assert!(logs_cmd.build);
3945 assert!(logs_cmd.failed);
3946 assert_eq!(logs_cmd.script_name.as_deref(), Some("xbp-production"));
3947 }
3948 _ => panic!("expected workers logs command"),
3949 }
3950 }
3951
3952 #[test]
3953 fn parses_worker_alias_command() {
3954 let cli = Cli::parse_from(["xbp", "worker", "env", "--show-values"]);
3955
3956 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3957 panic!("expected workers command through alias");
3958 };
3959
3960 match workers_cmd.command {
3961 super::WorkersSubCommand::Env(env_cmd) => {
3962 assert!(env_cmd.show_values);
3963 }
3964 _ => panic!("expected workers env command"),
3965 }
3966 }
3967
3968 #[test]
3969 fn parses_dns_providers_command() {
3970 let cli = Cli::parse_from(["xbp", "dns", "providers"]);
3971
3972 match cli.command {
3973 Some(Commands::Dns(dns_cmd)) => {
3974 assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
3975 }
3976 _ => panic!("expected dns command"),
3977 }
3978 }
3979
3980 #[test]
3981 fn dns_zone_list_defaults_provider_to_cloudflare() {
3982 let cli = Cli::parse_from(["xbp", "dns", "zones", "list"]);
3983
3984 let Some(Commands::Dns(dns_cmd)) = cli.command else {
3985 panic!("expected dns command");
3986 };
3987
3988 match dns_cmd.command {
3989 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
3990 DnsZonesSubCommand::List(list_cmd) => {
3991 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
3992 }
3993 _ => panic!("expected zones list command"),
3994 },
3995 _ => panic!("expected zones command"),
3996 }
3997 }
3998
3999 #[test]
4000 fn dns_record_list_defaults_provider_to_cloudflare() {
4001 let cli = Cli::parse_from(["xbp", "dns", "records", "list", "--zone-id", "zone_123"]);
4002
4003 let Some(Commands::Dns(dns_cmd)) = cli.command else {
4004 panic!("expected dns command");
4005 };
4006
4007 match dns_cmd.command {
4008 DnsSubCommand::Records(records_cmd) => match records_cmd.command {
4009 super::DnsRecordsSubCommand::List(list_cmd) => {
4010 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4011 assert_eq!(list_cmd.zone_id, "zone_123");
4012 }
4013 _ => panic!("expected records list command"),
4014 },
4015 _ => panic!("expected records command"),
4016 }
4017 }
4018
4019 #[test]
4020 fn dns_help_includes_descriptions_and_examples() {
4021 let err = Cli::try_parse_from(["xbp", "dns", "-h"]).expect_err("help");
4022 let rendered = err.to_string();
4023
4024 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
4025 assert!(rendered.contains("Manage DNS providers, zones, records, DNSSEC, and settings"));
4026 assert!(rendered.contains("List supported DNS providers and current implementation status"));
4027 assert!(rendered.contains("xbp dns records create"));
4028 }
4029
4030 #[test]
4031 fn dns_providers_help_includes_discovery_note() {
4032 let err = Cli::try_parse_from(["xbp", "dns", "providers", "-h"]).expect_err("help");
4033 let rendered = err.to_string();
4034
4035 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
4036 assert!(rendered.contains("Implemented providers are wired into `xbp dns` today."));
4037 }
4038
4039 #[test]
4040 fn dns_records_without_subcommand_displays_help_screen() {
4041 let err = Cli::try_parse_from(["xbp", "dns", "records"]).expect_err("missing subcommand");
4042 let rendered = err.to_string();
4043
4044 assert!(matches!(
4045 err.kind(),
4046 clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
4047 | clap::error::ErrorKind::MissingSubcommand
4048 ));
4049 assert!(rendered.contains("List, create, edit, import, export, and batch DNS records"));
4050 assert!(rendered.contains("Create a new DNS record"));
4051 assert!(rendered.contains("xbp dns records import"));
4052 }
4053
4054 #[test]
4055 fn parses_dns_zone_list_command() {
4056 let cli = Cli::parse_from([
4057 "xbp",
4058 "dns",
4059 "zones",
4060 "list",
4061 "--provider",
4062 "cloudflare",
4063 "--account-name-op",
4064 "contains",
4065 "--type",
4066 "full,partial",
4067 ]);
4068
4069 match cli.command {
4070 Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
4071 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
4072 DnsZonesSubCommand::List(list_cmd) => {
4073 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4074 assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
4075 assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
4076 }
4077 _ => panic!("expected dns zones list"),
4078 },
4079 _ => panic!("expected dns zones"),
4080 },
4081 _ => panic!("expected dns command"),
4082 }
4083 }
4084
4085 #[test]
4086 fn parses_domains_search_command() {
4087 let cli = Cli::parse_from([
4088 "xbp",
4089 "domains",
4090 "search",
4091 "--query",
4092 "xbp",
4093 "--extension",
4094 "com",
4095 ]);
4096
4097 match cli.command {
4098 Some(Commands::Domains(domains_cmd)) => {
4099 assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
4100 assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
4101 }
4102 _ => panic!("expected domains command"),
4103 }
4104 }
4105
4106 #[test]
4107 fn parses_cloudflare_config_account_id_command() {
4108 let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
4109
4110 match cli.command {
4111 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
4112 Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
4113 assert!(matches!(
4114 cloudflare_cmd.action,
4115 Some(CloudflareConfigAction::SetAccountId { .. })
4116 ));
4117 }
4118 _ => panic!("expected cloudflare config provider"),
4119 },
4120 _ => panic!("expected config command"),
4121 }
4122 }
4123}