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 --tray\n xbp mcp serve --detach\n xbp mcp config\n xbp mcp inspector --json\n xbp mcp status\n xbp mcp install --port 1113\n\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 #[arg(
1293 long,
1294 help = "Show a system tray icon (xbp.app icon) with a Quit action"
1295 )]
1296 pub tray: bool,
1297}
1298
1299#[derive(Args, Debug)]
1300pub struct McpInstallCmd {
1301 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1302 pub bind: String,
1303 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1304 pub port: u16,
1305}
1306
1307#[derive(Args, Debug)]
1308pub struct McpStatusCmd {
1309 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1310 pub bind: String,
1311 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1312 pub port: u16,
1313}
1314
1315#[derive(Args, Debug)]
1316pub struct McpConfigCmd {
1317 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1318 pub bind: String,
1319 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1320 pub port: u16,
1321}
1322
1323#[derive(Args, Debug)]
1324pub struct McpInspectorCmd {
1325 #[arg(
1326 long,
1327 default_value = "127.0.0.1",
1328 help = "Bind address for the xbp MCP server"
1329 )]
1330 pub bind: String,
1331 #[arg(
1332 long,
1333 default_value_t = 1113,
1334 help = "HTTP port for the xbp MCP SSE transport"
1335 )]
1336 pub port: u16,
1337 #[arg(long, default_value_t = 6274, help = "MCP Inspector UI port")]
1338 pub inspector_port: u16,
1339 #[arg(
1340 long,
1341 help = "Write .xbp/mcp-inspector.json for `npx @modelcontextprotocol/inspector --config`"
1342 )]
1343 pub write_config: bool,
1344 #[arg(
1345 long,
1346 help = "Launch MCP Inspector via npx (writes config first; requires Node.js)"
1347 )]
1348 pub launch: bool,
1349 #[arg(long, help = "Emit machine-readable JSON playbook")]
1350 pub json: bool,
1351}
1352
1353#[derive(Args, Debug, Clone, Default)]
1354pub struct ApiTargetOptions {
1355 #[arg(long, help = "Override the request base URL for this command")]
1356 pub base_url: Option<String>,
1357 #[arg(
1358 long,
1359 help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
1360 )]
1361 pub web: bool,
1362 #[arg(
1363 long,
1364 help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
1365 )]
1366 pub no_auth: bool,
1367 #[arg(
1368 long,
1369 help = "Extra header in 'Name: Value' format",
1370 value_name = "HEADER"
1371 )]
1372 pub header: Vec<String>,
1373 #[arg(long, help = "Print response headers")]
1374 pub include_headers: bool,
1375 #[arg(
1376 long,
1377 help = "Print the response body as-is without JSON pretty formatting"
1378 )]
1379 pub raw: bool,
1380}
1381
1382#[cfg(feature = "docker")]
1383#[derive(Args, Debug)]
1384pub struct DockerCmd {
1385 #[arg(
1386 trailing_var_arg = true,
1387 allow_hyphen_values = true,
1388 help = "Arguments to pass directly to the Docker CLI (default: --help)"
1389 )]
1390 pub args: Vec<String>,
1391}
1392
1393#[derive(Subcommand, Debug)]
1394pub enum ApiSubCommand {
1395 #[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
1396 Install {
1397 #[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
1398 port: u16,
1399 },
1400 #[command(about = "Call the XBP API health endpoint")]
1401 Health(ApiHealthCmd),
1402 #[command(about = "Manage XBP control-plane projects")]
1403 Projects(ApiProjectsCmd),
1404 #[command(about = "Manage XBP daemon registrations and heartbeats")]
1405 Daemons(ApiDaemonsCmd),
1406 #[command(about = "Manage XBP deployment jobs")]
1407 Jobs(ApiJobsCmd),
1408 #[command(about = "Manage XBP proxy routes on the local API server")]
1409 Routes(ApiRoutesCmd),
1410 #[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
1411 Request(ApiRequestCmd),
1412}
1413
1414#[derive(Args, Debug)]
1415pub struct ApiHealthCmd {
1416 #[command(flatten)]
1417 pub target: ApiTargetOptions,
1418}
1419
1420#[derive(Args, Debug)]
1421pub struct ApiProjectsCmd {
1422 #[command(subcommand)]
1423 pub command: ApiProjectsSubCommand,
1424}
1425
1426#[derive(Subcommand, Debug)]
1427pub enum ApiProjectsSubCommand {
1428 #[command(about = "List projects from the XBP control-plane API")]
1429 List(ApiProjectsListCmd),
1430 #[command(about = "Create or upsert a control-plane project")]
1431 Create(Box<ApiProjectsCreateCmd>),
1432}
1433
1434#[derive(Args, Debug)]
1435pub struct ApiProjectsListCmd {
1436 #[arg(long, help = "Optional organization ID filter")]
1437 pub organization_id: Option<String>,
1438 #[command(flatten)]
1439 pub target: ApiTargetOptions,
1440}
1441
1442#[derive(Args, Debug)]
1443pub struct ApiProjectsCreateCmd {
1444 #[arg(long, help = "Project name")]
1445 pub name: String,
1446 #[arg(long, help = "Project path or repo path key")]
1447 pub path: String,
1448 #[arg(long, help = "Optional organization ID")]
1449 pub organization_id: Option<String>,
1450 #[arg(long, help = "Optional project slug")]
1451 pub slug: Option<String>,
1452 #[arg(long, help = "Optional project version")]
1453 pub version: Option<String>,
1454 #[arg(long, help = "Optional build directory")]
1455 pub build_dir: Option<String>,
1456 #[arg(long, help = "Optional runtime enum value")]
1457 pub runtime: Option<String>,
1458 #[arg(long, help = "Optional default branch")]
1459 pub default_branch: Option<String>,
1460 #[arg(long, help = "Optional repository root directory")]
1461 pub root_directory: Option<String>,
1462 #[arg(long, help = "Optional build command")]
1463 pub build_command: Option<String>,
1464 #[arg(long, help = "Optional install command")]
1465 pub install_command: Option<String>,
1466 #[arg(long, help = "Optional start command")]
1467 pub start_command: Option<String>,
1468 #[arg(long, help = "Optional output directory")]
1469 pub output_directory: Option<String>,
1470 #[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
1471 pub repository_json: Option<String>,
1472 #[arg(long, help = "Runtime policy JSON payload")]
1473 pub runtime_policy_json: Option<String>,
1474 #[arg(long, help = "Metadata JSON object")]
1475 pub metadata_json: Option<String>,
1476 #[command(flatten)]
1477 pub target: ApiTargetOptions,
1478}
1479
1480#[derive(Args, Debug)]
1481pub struct ApiDaemonsCmd {
1482 #[command(subcommand)]
1483 pub command: ApiDaemonsSubCommand,
1484}
1485
1486#[derive(Subcommand, Debug)]
1487pub enum ApiDaemonsSubCommand {
1488 #[command(about = "List registered daemons")]
1489 List(ApiDaemonsListCmd),
1490 #[command(about = "Register or upsert a daemon record")]
1491 Register(ApiDaemonsRegisterCmd),
1492 #[command(about = "Post a heartbeat update for a daemon")]
1493 Heartbeat(ApiDaemonsHeartbeatCmd),
1494 #[command(about = "Update daemon status only")]
1495 UpdateStatus(ApiDaemonsUpdateStatusCmd),
1496}
1497
1498#[derive(Args, Debug)]
1499pub struct ApiDaemonsListCmd {
1500 #[command(flatten)]
1501 pub target: ApiTargetOptions,
1502}
1503
1504#[derive(Args, Debug)]
1505pub struct ApiDaemonsRegisterCmd {
1506 #[arg(long, help = "Daemon node name")]
1507 pub node_name: String,
1508 #[arg(long, help = "Daemon hostname")]
1509 pub hostname: String,
1510 #[arg(long, help = "Daemon binary version")]
1511 pub version: String,
1512 #[arg(long, help = "Optional region")]
1513 pub region: Option<String>,
1514 #[arg(long, help = "Optional public IP")]
1515 pub public_ip: Option<String>,
1516 #[arg(long, help = "Optional internal IP")]
1517 pub internal_ip: Option<String>,
1518 #[arg(long, help = "Optional status enum value")]
1519 pub status: Option<String>,
1520 #[arg(long, help = "Optional CPU core count")]
1521 pub cpu_cores: Option<i32>,
1522 #[arg(long, help = "Optional total memory in MB")]
1523 pub memory_total_mb: Option<i32>,
1524 #[arg(long, help = "Optional total disk in GB")]
1525 pub disk_total_gb: Option<i32>,
1526 #[arg(long, help = "Labels JSON object")]
1527 pub labels_json: Option<String>,
1528 #[arg(long, help = "Metadata JSON object")]
1529 pub metadata_json: Option<String>,
1530 #[command(flatten)]
1531 pub target: ApiTargetOptions,
1532}
1533
1534#[derive(Args, Debug)]
1535pub struct ApiDaemonsHeartbeatCmd {
1536 #[arg(help = "Daemon ID")]
1537 pub daemon_id: String,
1538 #[arg(long, help = "Optional status enum value")]
1539 pub status: Option<String>,
1540 #[arg(long, help = "Optional daemon version")]
1541 pub version: Option<String>,
1542 #[arg(long, help = "Optional public IP")]
1543 pub public_ip: Option<String>,
1544 #[arg(long, help = "Optional internal IP")]
1545 pub internal_ip: Option<String>,
1546 #[arg(long, help = "Optional CPU core count")]
1547 pub cpu_cores: Option<i32>,
1548 #[arg(long, help = "Optional total memory in MB")]
1549 pub memory_total_mb: Option<i32>,
1550 #[arg(long, help = "Optional total disk in GB")]
1551 pub disk_total_gb: Option<i32>,
1552 #[arg(long, help = "Labels JSON object")]
1553 pub labels_json: Option<String>,
1554 #[command(flatten)]
1555 pub target: ApiTargetOptions,
1556}
1557
1558#[derive(Args, Debug)]
1559pub struct ApiDaemonsUpdateStatusCmd {
1560 #[arg(help = "Daemon ID")]
1561 pub daemon_id: String,
1562 #[arg(long, help = "Daemon status enum value")]
1563 pub status: String,
1564 #[command(flatten)]
1565 pub target: ApiTargetOptions,
1566}
1567
1568#[derive(Args, Debug)]
1569pub struct ApiJobsCmd {
1570 #[command(subcommand)]
1571 pub command: ApiJobsSubCommand,
1572}
1573
1574#[derive(Subcommand, Debug)]
1575pub enum ApiJobsSubCommand {
1576 #[command(about = "List deployment jobs")]
1577 List(ApiJobsListCmd),
1578 #[command(about = "Create a deployment job for a project")]
1579 Create(ApiJobsCreateCmd),
1580 #[command(about = "Claim the next deployment job for a daemon")]
1581 Claim(ApiJobsClaimCmd),
1582 #[command(about = "Update deployment job status")]
1583 Update(ApiJobsUpdateCmd),
1584}
1585
1586#[derive(Args, Debug)]
1587pub struct ApiJobsListCmd {
1588 #[arg(long, help = "Optional project ID filter")]
1589 pub project_id: Option<String>,
1590 #[arg(long, help = "Optional deployment ID filter")]
1591 pub deployment_id: Option<String>,
1592 #[arg(long, help = "Optional daemon ID filter")]
1593 pub daemon_id: Option<String>,
1594 #[arg(long, help = "Optional status filter")]
1595 pub status: Option<String>,
1596 #[arg(long, help = "Optional result limit")]
1597 pub limit: Option<usize>,
1598 #[command(flatten)]
1599 pub target: ApiTargetOptions,
1600}
1601
1602#[derive(Args, Debug)]
1603pub struct ApiJobsCreateCmd {
1604 #[arg(long, help = "Project ID")]
1605 pub project_id: String,
1606 #[arg(long, help = "Deployment ID")]
1607 pub deployment_id: String,
1608 #[arg(long, help = "Optional daemon ID assignment")]
1609 pub daemon_id: Option<String>,
1610 #[arg(long, help = "Optional priority")]
1611 pub priority: Option<i32>,
1612 #[arg(long, help = "Optional max attempts")]
1613 pub max_attempts: Option<i32>,
1614 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1615 pub run_after: Option<String>,
1616 #[arg(long, help = "Optional payload JSON object")]
1617 pub payload_json: Option<String>,
1618 #[command(flatten)]
1619 pub target: ApiTargetOptions,
1620}
1621
1622#[derive(Args, Debug)]
1623pub struct ApiJobsClaimCmd {
1624 #[arg(long, help = "Daemon ID claiming work")]
1625 pub daemon_id: String,
1626 #[arg(long, help = "Optional lock owner")]
1627 pub locked_by: Option<String>,
1628 #[command(flatten)]
1629 pub target: ApiTargetOptions,
1630}
1631
1632#[derive(Args, Debug)]
1633pub struct ApiJobsUpdateCmd {
1634 #[arg(help = "Deployment job ID")]
1635 pub job_id: String,
1636 #[arg(long, help = "Deployment job status enum value")]
1637 pub status: String,
1638 #[arg(long, help = "Optional error text")]
1639 pub error_text: Option<String>,
1640 #[command(flatten)]
1641 pub target: ApiTargetOptions,
1642}
1643
1644#[derive(Args, Debug)]
1645pub struct ApiRoutesCmd {
1646 #[command(subcommand)]
1647 pub command: ApiRoutesSubCommand,
1648}
1649
1650#[derive(Subcommand, Debug)]
1651pub enum ApiRoutesSubCommand {
1652 #[command(about = "List configured proxy routes")]
1653 List(ApiRoutesListCmd),
1654 #[command(about = "Create or replace a proxy route")]
1655 Create(ApiRoutesCreateCmd),
1656 #[command(about = "Delete a proxy route by domain")]
1657 Delete(ApiRoutesDeleteCmd),
1658}
1659
1660#[derive(Args, Debug)]
1661pub struct ApiRoutesListCmd {
1662 #[command(flatten)]
1663 pub target: ApiTargetOptions,
1664}
1665
1666#[derive(Args, Debug)]
1667pub struct ApiRoutesCreateCmd {
1668 #[arg(long, help = "Domain name for the route")]
1669 pub domain: String,
1670 #[arg(long, help = "Upstream target URL", required = true)]
1671 pub target: Vec<String>,
1672 #[arg(
1673 long,
1674 help = "Weighted upstream target in url=weight form",
1675 value_name = "URL=WEIGHT"
1676 )]
1677 pub weighted_target: Vec<String>,
1678 #[arg(long, help = "Optional header condition")]
1679 pub header_condition: Option<String>,
1680 #[arg(long, help = "Optional path prefix condition")]
1681 pub path_prefix: Option<String>,
1682 #[command(flatten)]
1683 pub target_options: ApiTargetOptions,
1684}
1685
1686#[derive(Args, Debug)]
1687pub struct ApiRoutesDeleteCmd {
1688 #[arg(help = "Domain name for the route")]
1689 pub domain: String,
1690 #[command(flatten)]
1691 pub target: ApiTargetOptions,
1692}
1693
1694#[derive(Args, Debug)]
1695pub struct ApiRequestCmd {
1696 #[arg(help = "Request path like /projects or a full https:// URL")]
1697 pub path: String,
1698 #[arg(
1699 short = 'X',
1700 long,
1701 help = "HTTP method to use (default: GET, or POST when a body is provided)"
1702 )]
1703 pub method: Option<String>,
1704 #[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
1705 pub body: Option<String>,
1706 #[arg(long, help = "Read the request body from a file")]
1707 pub body_file: Option<PathBuf>,
1708 #[command(flatten)]
1709 pub target: ApiTargetOptions,
1710}
1711#[derive(Args, Debug)]
1712pub struct TailCmd {
1713 #[arg(long, help = "Tail Kafka topic instead of log files")]
1714 pub kafka: bool,
1715 #[arg(long, help = "Ship logs to Kafka")]
1716 pub ship: bool,
1717}
1718
1719#[derive(Args, Debug)]
1720#[command(
1721 arg_required_else_help = true,
1722 disable_help_subcommand = true,
1723 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1724 after_help = crate::cli::help_render::GENERATE_AFTER_HELP
1725)]
1726pub struct GenerateCmd {
1727 #[command(subcommand)]
1728 pub command: GenerateSubCommand,
1729}
1730
1731#[derive(Subcommand, Debug)]
1732pub enum GenerateSubCommand {
1733 #[command(about = "Generate or update .xbp/xbp.yaml (and convert legacy JSON)")]
1734 Config(GenerateConfigCmd),
1735 Systemd(GenerateSystemdCmd),
1736}
1737
1738#[derive(Args, Debug)]
1739pub struct GenerateConfigCmd {
1740 #[arg(
1741 long,
1742 help = "Overwrite .xbp/xbp.yaml if it already exists (default errors when present)"
1743 )]
1744 pub force: bool,
1745 #[arg(
1746 long,
1747 help = "Refresh .xbp/xbp.yaml by applying project detection defaults for missing fields"
1748 )]
1749 pub update: bool,
1750 #[arg(
1751 long,
1752 help = "Path to a legacy xbp.json file to convert into .xbp/xbp.yaml"
1753 )]
1754 pub from_json: Option<PathBuf>,
1755}
1756
1757#[derive(Args, Debug)]
1758#[command(
1759 arg_required_else_help = true,
1760 disable_help_subcommand = true,
1761 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1762 after_help = "Examples:\n xbp workers list\n xbp workers list --failed -n 10\n xbp workers ls\n xbp workers logs -f\n xbp workers logs --build --failed -n 200 -o build.log\n xbp workers logs --build --grep error --errors-only\n xbp workers secrets put --environment production --name GITHUB_APP_PRIVATE_KEY --from-stdin\n xbp workers secrets list --environment production\n xbp workers settings get --environment production\n xbp workers wrangler generate-config --output wrangler.deploy.json\n xbp workers d1 migrations apply DB --remote\n xbp workers deploy sync-env-local\n xbp workers deploy ci --version-upload\n xbp workers worktree link-dev-vars"
1763)]
1764pub struct WorkersCmd {
1765 #[arg(
1766 long,
1767 help = "Workers project root (defaults to current dir, or apps/web inside the current XBP repo when present)"
1768 )]
1769 pub root: Option<PathBuf>,
1770 #[arg(long, help = "Cloudflare API token override")]
1771 pub token: Option<String>,
1772 #[arg(long, help = "Cloudflare account ID override")]
1773 pub account_id: Option<String>,
1774 #[command(subcommand)]
1775 pub command: WorkersSubCommand,
1776}
1777
1778#[derive(Subcommand, Debug)]
1779pub enum WorkersSubCommand {
1780 #[command(
1781 alias = "ls",
1782 about = "List Cloudflare Worker scripts with build and placement status",
1783 after_help = "Examples:\n xbp workers list\n xbp workers list --all\n xbp workers list --failed -n 10\n xbp workers list --status running --sort modified\n xbp workers list --json"
1784 )]
1785 List(WorkersListCmd),
1786 #[command(
1787 about = "Show deployment status, stream runtime logs, or fetch Workers Builds CI logs",
1788 after_help = "Examples:\n xbp workers logs --worker suits-formations --json\n xbp workers logs -f\n xbp workers logs -n 100 -o runtime.log\n xbp workers logs xbp-production\n xbp workers logs --build --wait\n xbp workers logs --build --failed -n 200\n xbp workers logs --build --grep error --errors-only\n xbp workers logs --build --list-builds"
1789 )]
1790 Logs(WorkersLogsCmd),
1791 #[command(about = "Manage secret bindings for a Worker script or Wrangler environment")]
1792 Secrets(WorkersSecretsCmd),
1793 #[command(about = "Fetch remote Worker settings via the Cloudflare API")]
1794 Settings(WorkersSettingsCmd),
1795 #[command(about = "Inspect or generate Wrangler config helpers")]
1796 Wrangler(WorkersWranglerCmd),
1797 #[command(about = "Run Wrangler D1 migration helpers")]
1798 D1(WorkersD1Cmd),
1799 #[command(about = "Run Worker deploy and predeploy helpers")]
1800 Deploy(WorkersDeployCmd),
1801 #[command(about = "Inspect shared worktree paths or link shared dev files")]
1802 Worktree(WorkersWorktreeCmd),
1803 #[command(about = "Show resolved Worker runtime metadata from local env and config files")]
1804 Env(WorkersEnvCmd),
1805}
1806
1807#[derive(Args, Debug, Default)]
1808pub struct WorkersListCmd {
1809 #[arg(
1810 long,
1811 help = "Show every Worker in the account instead of only Workers for this XBP project"
1812 )]
1813 pub all: bool,
1814 #[arg(long, help = "Output the worker inventory as JSON")]
1815 pub json: bool,
1816 #[arg(short = 'n', long = "limit", help = "Show at most N workers")]
1817 pub limit: Option<usize>,
1818 #[arg(
1819 long = "status",
1820 help = "Filter by build status (failed, success, running, unknown)"
1821 )]
1822 pub status: Option<String>,
1823 #[arg(long, help = "Shortcut for --status failed")]
1824 pub failed: bool,
1825 #[arg(
1826 long = "sort",
1827 default_value = "name",
1828 help = "Sort workers by name, modified, or status"
1829 )]
1830 pub sort: String,
1831 #[arg(long = "no-color", help = "Disable ANSI colors in table output")]
1832 pub no_color: bool,
1833}
1834
1835#[derive(Args, Debug)]
1836pub struct WorkersLogsCmd {
1837 #[command(flatten)]
1838 pub target: WorkersTargetArgs,
1839 #[arg(
1840 short = 'f',
1841 long = "follow",
1842 help = "Stream runtime logs until interrupted (wrangler tail)"
1843 )]
1844 pub follow: bool,
1845 #[arg(
1846 long = "build",
1847 help = "Show Workers Builds CI logs instead of runtime deployment output"
1848 )]
1849 pub build: bool,
1850 #[arg(
1851 long = "failed",
1852 help = "When using --build, prefer the latest failed build"
1853 )]
1854 pub failed: bool,
1855 #[arg(long, help = "Output logs as JSON")]
1856 pub json: bool,
1857 #[arg(
1858 short = 'n',
1859 long = "lines",
1860 help = "Show only the last N log lines (deployments/build output)"
1861 )]
1862 pub lines: Option<usize>,
1863 #[arg(
1864 short = 'o',
1865 long = "output",
1866 help = "Write log output to a file instead of stdout"
1867 )]
1868 pub output: Option<PathBuf>,
1869 #[arg(
1870 short = 'g',
1871 long = "grep",
1872 help = "Filter log lines matching this pattern (case-insensitive substring)"
1873 )]
1874 pub grep: Option<String>,
1875 #[arg(
1876 short = 'e',
1877 long = "errors-only",
1878 help = "Show only lines that look like errors or failures"
1879 )]
1880 pub errors_only: bool,
1881 #[arg(long = "no-color", help = "Disable ANSI colors in log output")]
1882 pub no_color: bool,
1883 #[arg(
1884 long = "list-builds",
1885 help = "List recent Workers Builds before fetching logs (with --build)"
1886 )]
1887 pub list_builds: bool,
1888 #[arg(
1889 long = "build-index",
1890 help = "Select build by index from --list-builds (0 = latest)"
1891 )]
1892 pub build_index: Option<usize>,
1893 #[arg(
1894 long,
1895 help = "Poll until the selected Workers Build finishes before fetching logs"
1896 )]
1897 pub wait: bool,
1898 #[arg(long = "no-wait", help = "Skip polling for in-progress Workers Builds")]
1899 pub no_wait: bool,
1900 #[arg(
1901 long = "wait-seconds",
1902 default_value_t = 120,
1903 help = "Maximum seconds to poll an in-progress Workers Build"
1904 )]
1905 pub wait_seconds: u64,
1906 #[arg(help = "Worker script name. When omitted, xbp prompts interactively.")]
1907 pub script_name: Option<String>,
1908}
1909
1910#[derive(Args, Debug, Clone, Default)]
1911pub struct WorkersTargetArgs {
1912 #[arg(
1913 long,
1914 help = "Worker base name (defaults to wrangler config name or xbp)"
1915 )]
1916 pub worker: Option<String>,
1917 #[arg(
1918 long = "environment",
1919 alias = "env",
1920 help = "Wrangler environment name. The remote script resolves to <worker>-<environment>."
1921 )]
1922 pub environment: Option<String>,
1923 #[arg(
1924 long,
1925 help = "Exact remote script name override. When set, this bypasses <worker>-<environment> resolution."
1926 )]
1927 pub script: Option<String>,
1928}
1929
1930#[derive(Args, Debug)]
1931pub struct WorkersSecretsCmd {
1932 #[command(flatten)]
1933 pub target: WorkersTargetArgs,
1934 #[command(subcommand)]
1935 pub command: WorkersSecretsSubCommand,
1936}
1937
1938#[derive(Subcommand, Debug)]
1939pub enum WorkersSecretsSubCommand {
1940 #[command(about = "List secret bindings on the resolved Worker script")]
1941 List(WorkersSecretsListCmd),
1942 #[command(about = "Fetch one secret binding metadata or value")]
1943 Get(WorkersSecretsGetCmd),
1944 #[command(about = "Create or update a secret binding")]
1945 Put(WorkersSecretsPutCmd),
1946 #[command(about = "Delete a secret binding")]
1947 Delete(WorkersSecretsDeleteCmd),
1948 #[command(about = "Create, update, or delete multiple secret bindings from a file")]
1949 Bulk(WorkersSecretsBulkCmd),
1950}
1951
1952#[derive(Args, Debug, Default)]
1953pub struct WorkersSecretsListCmd {}
1954
1955#[derive(Args, Debug)]
1956pub struct WorkersSecretsGetCmd {
1957 #[arg(long, help = "Secret binding name")]
1958 pub name: String,
1959}
1960
1961#[derive(Args, Debug)]
1962pub struct WorkersSecretsPutCmd {
1963 #[arg(long, help = "Secret binding name")]
1964 pub name: String,
1965 #[arg(long, help = "Secret value")]
1966 pub value: Option<String>,
1967 #[arg(long, help = "Read the secret value from stdin instead of --value")]
1968 pub from_stdin: bool,
1969}
1970
1971#[derive(Args, Debug)]
1972pub struct WorkersSecretsDeleteCmd {
1973 #[arg(long, help = "Secret binding name")]
1974 pub name: String,
1975}
1976
1977#[derive(Args, Debug)]
1978pub struct WorkersSecretsBulkCmd {
1979 #[arg(long, help = "Path to a .env or JSON file containing secret updates")]
1980 pub file: PathBuf,
1981 #[arg(
1982 long,
1983 default_value = "env",
1984 help = "Input format: env or json. For json, pass an object mapping names to string values or null for deletes."
1985 )]
1986 pub format: String,
1987}
1988
1989#[derive(Args, Debug)]
1990pub struct WorkersSettingsCmd {
1991 #[command(flatten)]
1992 pub target: WorkersTargetArgs,
1993}
1994
1995#[derive(Args, Debug)]
1996pub struct WorkersWranglerCmd {
1997 #[command(subcommand)]
1998 pub command: WorkersWranglerSubCommand,
1999}
2000
2001#[derive(Subcommand, Debug)]
2002pub enum WorkersWranglerSubCommand {
2003 #[command(about = "Generate a Wrangler deploy config JSON file from env vars")]
2004 GenerateConfig(WorkersWranglerGenerateConfigCmd),
2005 #[command(about = "Resolve which Wrangler config file local dev should use")]
2006 ConfigPath(WorkersWranglerConfigPathCmd),
2007}
2008
2009#[derive(Args, Debug)]
2010pub struct WorkersWranglerGenerateConfigCmd {
2011 #[arg(
2012 long,
2013 default_value = "wrangler.deploy.json",
2014 help = "Output filename, relative to the worker root unless absolute"
2015 )]
2016 pub output: PathBuf,
2017}
2018
2019#[derive(Args, Debug)]
2020pub struct WorkersWranglerConfigPathCmd {
2021 #[arg(
2022 long,
2023 default_value = "serve",
2024 help = "Calling command name, for example serve"
2025 )]
2026 pub command_name: String,
2027 #[arg(
2028 long,
2029 default_value = "development",
2030 help = "Execution mode, for example development or production"
2031 )]
2032 pub mode: String,
2033}
2034
2035#[derive(Args, Debug)]
2036pub struct WorkersD1Cmd {
2037 #[command(subcommand)]
2038 pub command: WorkersD1SubCommand,
2039}
2040
2041#[derive(Subcommand, Debug)]
2042pub enum WorkersD1SubCommand {
2043 #[command(about = "Apply pending Wrangler D1 migrations")]
2044 Migrations(WorkersD1MigrationsCmd),
2045}
2046
2047#[derive(Args, Debug)]
2048pub struct WorkersD1MigrationsCmd {
2049 #[command(subcommand)]
2050 pub command: WorkersD1MigrationsSubCommand,
2051}
2052
2053#[derive(Subcommand, Debug)]
2054pub enum WorkersD1MigrationsSubCommand {
2055 #[command(about = "Apply pending migrations to a local or remote D1 database")]
2056 Apply(WorkersD1MigrationsApplyCmd),
2057}
2058
2059#[derive(Args, Debug)]
2060pub struct WorkersD1MigrationsApplyCmd {
2061 #[arg(help = "D1 database binding or name, for example DB")]
2062 pub database: String,
2063 #[arg(
2064 long,
2065 conflicts_with = "remote",
2066 help = "Apply migrations to the local Wrangler D1 database"
2067 )]
2068 pub local: bool,
2069 #[arg(
2070 long,
2071 conflicts_with = "local",
2072 help = "Apply migrations to the remote D1 database"
2073 )]
2074 pub remote: bool,
2075 #[arg(long, help = "Wrangler config path override")]
2076 pub config: Option<PathBuf>,
2077 #[command(flatten)]
2078 pub target: WorkersTargetArgs,
2079 #[arg(
2080 long,
2081 help = "Persist local D1 state to this directory. When omitted in a git worktree, xbp uses the shared .wrangler/state path automatically."
2082 )]
2083 pub persist_to: Option<PathBuf>,
2084 #[arg(
2085 long,
2086 help = "Disable the automatic shared .wrangler/state path when running local migrations from a git worktree"
2087 )]
2088 pub no_shared_worktree_state: bool,
2089}
2090
2091#[derive(Args, Debug)]
2092pub struct WorkersDeployCmd {
2093 #[command(subcommand)]
2094 pub command: WorkersDeploySubCommand,
2095}
2096
2097#[derive(Subcommand, Debug)]
2098pub enum WorkersDeploySubCommand {
2099 #[command(about = "Run the predeploy sync flow unless Workers CI mode is active")]
2100 Predeploy(WorkersDeployPredeployCmd),
2101 #[command(about = "Read .env.local or process env, then emit .dev.vars and Wrangler configs")]
2102 SyncEnvLocal(WorkersDeploySyncEnvLocalCmd),
2103 #[command(about = "Run the existing Cloudflare CI deploy workflow")]
2104 Ci(WorkersDeployCiCmd),
2105 #[command(
2106 about = "Run the existing deploy-selection flow that chooses CI or local deploy behavior"
2107 )]
2108 Select(WorkersDeploySelectCmd),
2109}
2110
2111#[derive(Args, Debug)]
2112pub struct WorkersDeployPredeployCmd {
2113 #[arg(long, help = "Force Workers CI mode and skip local sync")]
2114 pub ci: bool,
2115}
2116
2117#[derive(Args, Debug)]
2118pub struct WorkersDeploySyncEnvLocalCmd {}
2119
2120#[derive(Args, Debug)]
2121pub struct WorkersDeployCiCmd {
2122 #[arg(long, help = "Upload a new version without immediately deploying it")]
2123 pub version_upload: bool,
2124}
2125
2126#[derive(Args, Debug)]
2127pub struct WorkersDeploySelectCmd {
2128 #[arg(long, help = "Force the WORKERS_CI=1 branch of the selector")]
2129 pub ci: bool,
2130 #[arg(
2131 long,
2132 help = "Branch name to expose as WORKERS_CI_BRANCH when --ci is set"
2133 )]
2134 pub branch: Option<String>,
2135}
2136
2137#[derive(Args, Debug)]
2138pub struct WorkersWorktreeCmd {
2139 #[command(subcommand)]
2140 pub command: WorkersWorktreeSubCommand,
2141}
2142
2143#[derive(Subcommand, Debug)]
2144pub enum WorkersWorktreeSubCommand {
2145 #[command(about = "Print repo-root, primary worktree, and shared Wrangler state paths")]
2146 Paths(WorkersWorktreePathsCmd),
2147 #[command(
2148 about = "Symlink apps/web/.dev.vars and wrangler.dev.jsonc from the primary worktree when in a linked worktree"
2149 )]
2150 LinkDevVars(WorkersWorktreeLinkDevVarsCmd),
2151}
2152
2153#[derive(Args, Debug, Default)]
2154pub struct WorkersWorktreePathsCmd {}
2155
2156#[derive(Args, Debug, Default)]
2157pub struct WorkersWorktreeLinkDevVarsCmd {}
2158
2159#[derive(Args, Debug)]
2160pub struct WorkersEnvCmd {
2161 #[command(flatten)]
2162 pub target: WorkersTargetArgs,
2163 #[arg(
2164 long,
2165 help = "Show resolved plain-text binding values instead of masking them"
2166 )]
2167 pub show_values: bool,
2168}
2169
2170#[cfg(feature = "secrets")]
2171#[derive(Args, Debug)]
2172pub struct SecretsCmd {
2173 #[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
2174 pub provider: SecretsProviderKind,
2175 #[arg(long, help = "GitHub repository override (owner/repo)")]
2176 pub repo: Option<String>,
2177 #[arg(
2178 long,
2179 help = "Provider token override (GitHub token or Cloudflare API token)"
2180 )]
2181 pub token: Option<String>,
2182 #[arg(long, help = "Cloudflare account ID override")]
2183 pub account_id: Option<String>,
2184 #[arg(
2185 long = "environment",
2186 alias = "env",
2187 default_value = "xbp-dev",
2188 help = "Environment to sync (default: xbp-dev). Nested services are scoped automatically, e.g. xbp-dev-web."
2189 )]
2190 pub environment: String,
2191 #[arg(
2192 long,
2193 help = "Service name from .xbp/xbp.yaml. If omitted, XBP resolves it from the current directory or prompts when ambiguous."
2194 )]
2195 pub service: Option<String>,
2196 #[command(subcommand)]
2197 pub command: Option<SecretsSubCommand>,
2198}
2199
2200#[cfg(feature = "secrets")]
2201#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2202pub enum SecretsProviderKind {
2203 Github,
2204 Cloudflare,
2205 Railway,
2206 Vercel,
2207}
2208
2209#[cfg(feature = "secrets")]
2210#[derive(Subcommand, Debug)]
2211pub enum SecretsSubCommand {
2212 #[command(alias = "ls", alias = "list-providers")]
2214 Providers,
2215 List(ListCmd),
2217 Push(PushCmd),
2219 Pull(PullCmd),
2221 GenerateDefault(GenerateDefaultCmd),
2223 GenerateExample(GenerateExampleCmd),
2225 Diff,
2227 Verify,
2229 #[command(name = "diag", alias = "doctor")]
2231 Diag,
2232 Stores(SecretsStoresCmd),
2234 Secrets(CloudflareSecretsCmd),
2236 Quota(SecretsQuotaCmd),
2238 #[command(name = "usage")]
2240 Usage,
2241}
2242
2243#[cfg(feature = "secrets")]
2244#[derive(Args, Debug)]
2245pub struct ListCmd {
2246 #[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
2247 pub file: Option<String>,
2248 #[arg(long, help = "Output format: plain (default) or json")]
2249 pub format: Option<String>,
2250}
2251
2252#[cfg(feature = "secrets")]
2253#[derive(Args, Debug)]
2254pub struct PushCmd {
2255 #[arg(long, help = "Path to env file (default: .env.local/.env)")]
2256 pub file: Option<String>,
2257 #[arg(
2258 long,
2259 help = "Force overwrite existing GitHub Actions environment variables"
2260 )]
2261 pub force: bool,
2262 #[arg(long, help = "Show what would be pushed without making changes")]
2263 pub dry_run: bool,
2264}
2265
2266#[cfg(feature = "secrets")]
2267#[derive(Args, Debug)]
2268pub struct PullCmd {
2269 #[arg(long, help = "Output file path (default: .env.local)")]
2270 pub output: Option<String>,
2271}
2272
2273#[cfg(feature = "secrets")]
2274#[derive(Args, Debug)]
2275pub struct GenerateDefaultCmd {
2276 #[arg(long, help = "Output file path (default: .env.default)")]
2277 pub output: Option<String>,
2278}
2279
2280#[cfg(feature = "secrets")]
2281#[derive(Args, Debug)]
2282pub struct GenerateExampleCmd {
2283 #[arg(long, help = "Output file path (default: .env.example)")]
2284 pub output: Option<String>,
2285 #[arg(long, help = "Remove keys from .env.local not in .env.example")]
2286 pub clean: bool,
2287 #[arg(long, help = "Only include vars matching prefix (repeatable)")]
2288 pub include_prefix: Vec<String>,
2289 #[arg(long, help = "Exclude vars matching prefix (repeatable)")]
2290 pub exclude_prefix: Vec<String>,
2291}
2292
2293#[cfg(feature = "secrets")]
2294#[derive(Args, Debug)]
2295pub struct SecretsStoresCmd {
2296 #[command(subcommand)]
2297 pub command: SecretsStoresSubCommand,
2298}
2299
2300#[cfg(feature = "secrets")]
2301#[derive(Subcommand, Debug)]
2302pub enum SecretsStoresSubCommand {
2303 List(CloudflareSecretsStoreListCmd),
2304 Get(CloudflareSecretsStoreGetCmd),
2305 Create(CloudflareSecretsStoreCreateCmd),
2306 Delete(CloudflareSecretsStoreDeleteCmd),
2307}
2308
2309#[cfg(feature = "secrets")]
2310#[derive(Args, Debug)]
2311pub struct CloudflareSecretsStoreListCmd {}
2312
2313#[cfg(feature = "secrets")]
2314#[derive(Args, Debug)]
2315pub struct CloudflareSecretsStoreGetCmd {
2316 #[arg(long)]
2317 pub store_id: String,
2318}
2319
2320#[cfg(feature = "secrets")]
2321#[derive(Args, Debug)]
2322pub struct CloudflareSecretsStoreCreateCmd {
2323 #[arg(long)]
2324 pub name: String,
2325}
2326
2327#[cfg(feature = "secrets")]
2328#[derive(Args, Debug)]
2329pub struct CloudflareSecretsStoreDeleteCmd {
2330 #[arg(long)]
2331 pub store_id: String,
2332}
2333
2334#[cfg(feature = "secrets")]
2335#[derive(Args, Debug)]
2336pub struct CloudflareSecretsCmd {
2337 #[command(subcommand)]
2338 pub command: CloudflareSecretsSubCommand,
2339}
2340
2341#[cfg(feature = "secrets")]
2342#[derive(Subcommand, Debug)]
2343pub enum CloudflareSecretsSubCommand {
2344 List(CloudflareSecretsListCmd),
2345 Get(CloudflareSecretsGetCmd),
2346 Create(CloudflareSecretsCreateCmd),
2347 Edit(CloudflareSecretsEditCmd),
2348 Delete(CloudflareSecretsDeleteCmd),
2349 #[command(name = "delete-bulk")]
2350 DeleteBulk(CloudflareSecretsBulkDeleteCmd),
2351 Duplicate(CloudflareSecretsDuplicateCmd),
2352}
2353
2354#[cfg(feature = "secrets")]
2355#[derive(Args, Debug)]
2356pub struct CloudflareSecretsListCmd {
2357 #[arg(long)]
2358 pub store_id: String,
2359}
2360
2361#[cfg(feature = "secrets")]
2362#[derive(Args, Debug)]
2363pub struct CloudflareSecretsGetCmd {
2364 #[arg(long)]
2365 pub store_id: String,
2366 #[arg(long)]
2367 pub secret_id: String,
2368}
2369
2370#[cfg(feature = "secrets")]
2371#[derive(Args, Debug)]
2372pub struct CloudflareSecretsCreateCmd {
2373 #[arg(long)]
2374 pub store_id: String,
2375 #[arg(long)]
2376 pub name: String,
2377 #[arg(long)]
2378 pub value: String,
2379 #[arg(long, value_delimiter = ',')]
2380 pub scopes: Vec<String>,
2381 #[arg(long)]
2382 pub comment: Option<String>,
2383}
2384
2385#[cfg(feature = "secrets")]
2386#[derive(Args, Debug)]
2387pub struct CloudflareSecretsEditCmd {
2388 #[arg(long)]
2389 pub store_id: String,
2390 #[arg(long)]
2391 pub secret_id: String,
2392 #[arg(long)]
2393 pub name: Option<String>,
2394 #[arg(long)]
2395 pub value: Option<String>,
2396 #[arg(long, value_delimiter = ',')]
2397 pub scopes: Vec<String>,
2398 #[arg(long)]
2399 pub comment: Option<String>,
2400}
2401
2402#[cfg(feature = "secrets")]
2403#[derive(Args, Debug)]
2404pub struct CloudflareSecretsDeleteCmd {
2405 #[arg(long)]
2406 pub store_id: String,
2407 #[arg(long)]
2408 pub secret_id: String,
2409}
2410
2411#[cfg(feature = "secrets")]
2412#[derive(Args, Debug)]
2413pub struct CloudflareSecretsBulkDeleteCmd {
2414 #[arg(long)]
2415 pub store_id: String,
2416 #[arg(long = "secret-id", required = true)]
2417 pub secret_ids: Vec<String>,
2418}
2419
2420#[cfg(feature = "secrets")]
2421#[derive(Args, Debug)]
2422pub struct CloudflareSecretsDuplicateCmd {
2423 #[arg(long)]
2424 pub store_id: String,
2425 #[arg(long)]
2426 pub secret_id: String,
2427 #[arg(long)]
2428 pub name: String,
2429 #[arg(long, value_delimiter = ',')]
2430 pub scopes: Vec<String>,
2431 #[arg(long)]
2432 pub comment: Option<String>,
2433}
2434
2435#[cfg(feature = "secrets")]
2436#[derive(Args, Debug)]
2437pub struct SecretsQuotaCmd {
2438 #[command(subcommand)]
2439 pub command: SecretsQuotaSubCommand,
2440}
2441
2442#[cfg(feature = "secrets")]
2443#[derive(Subcommand, Debug)]
2444pub enum SecretsQuotaSubCommand {
2445 Get(SecretsQuotaGetCmd),
2446}
2447
2448#[cfg(feature = "secrets")]
2449#[derive(Args, Debug)]
2450pub struct SecretsQuotaGetCmd {}
2451
2452const DNS_HELP_TEMPLATE: &str = crate::cli::help_render::XBP_HELP_TEMPLATE;
2453
2454const DNS_COMMAND_AFTER_HELP: &str = "\
2455Examples:
2456 xbp dns providers
2457 xbp dns zones list --provider cloudflare --account-id acc_123
2458 xbp dns records list --provider cloudflare --zone-id zone_123
2459 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
2460 xbp dns dnssec get --provider cloudflare --zone-id zone_123
2461 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
2462
2463Notes:
2464 Start with `xbp dns providers` to see what is implemented today.
2465 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`).";
2466
2467const DNS_ZONES_AFTER_HELP: &str = "\
2468Examples:
2469 xbp dns zones list --provider cloudflare --account-id acc_123
2470 xbp dns zones get --provider cloudflare --zone-id zone_123
2471 xbp dns zones create --provider cloudflare --name example.com --account-id acc_123 --jump-start
2472 xbp dns zones edit --provider cloudflare --zone-id zone_123 --paused true
2473 xbp dns zones delete --provider cloudflare --zone-id zone_123";
2474
2475const DNS_RECORDS_AFTER_HELP: &str = "\
2476Examples:
2477 xbp dns records list --provider cloudflare --zone-id zone_123
2478 xbp dns records get --provider cloudflare --zone-id zone_123 --record-id rec_123
2479 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
2480 xbp dns records edit --provider cloudflare --zone-id zone_123 --record-id rec_123 --proxied true
2481 xbp dns records import --provider cloudflare --zone-id zone_123 --file zone.txt
2482 xbp dns records export --provider cloudflare --zone-id zone_123 --output zone.txt";
2483
2484const DNS_DNSSEC_AFTER_HELP: &str = "\
2485Examples:
2486 xbp dns dnssec get --provider cloudflare --zone-id zone_123
2487 xbp dns dnssec edit --provider cloudflare --zone-id zone_123 --status active";
2488
2489const DNS_SETTINGS_AFTER_HELP: &str = "\
2490Examples:
2491 xbp dns settings get --provider cloudflare --zone-id zone_123
2492 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
2493 xbp dns settings edit --provider cloudflare --zone-id zone_123 --nameservers-type custom --nameservers-ns-set 2";
2494
2495const DNS_PROVIDERS_AFTER_HELP: &str = "\
2496Examples:
2497 xbp dns providers
2498
2499What this shows:
2500 Implemented providers are wired into `xbp dns` today.
2501 Planned providers are tracked in the CLI surface but not callable yet.";
2502
2503#[derive(Args, Debug)]
2504#[command(
2505 about = "Manage DNS providers, zones, records, DNSSEC, and provider-level settings",
2506 arg_required_else_help = true,
2507 disable_help_subcommand = true,
2508 help_template = DNS_HELP_TEMPLATE,
2509 after_help = DNS_COMMAND_AFTER_HELP
2510)]
2511pub struct DnsCmd {
2512 #[command(subcommand)]
2513 pub command: DnsSubCommand,
2514}
2515
2516#[derive(Subcommand, Debug)]
2517pub enum DnsSubCommand {
2518 #[command(
2519 alias = "ls",
2520 alias = "list",
2521 about = "List supported DNS providers and current implementation status",
2522 after_help = DNS_PROVIDERS_AFTER_HELP
2523 )]
2524 Providers,
2525 #[command(about = "Inspect and manage DNS zones")]
2526 Zones(DnsZonesCmd),
2527 #[command(about = "List, create, edit, import, export, and batch DNS records")]
2528 Records(DnsRecordsCmd),
2529 #[command(about = "Inspect or edit DNSSEC status for a zone")]
2530 Dnssec(DnssecCmd),
2531 #[command(about = "Inspect or edit provider DNS settings for a zone")]
2532 Settings(DnsSettingsCmd),
2533}
2534
2535#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2536pub enum DnsProviderKind {
2537 Cloudflare,
2538 Hetzner,
2539 Vercel,
2540 Custom,
2541}
2542
2543#[derive(Args, Debug)]
2544#[command(
2545 about = "Inspect and manage DNS zones",
2546 arg_required_else_help = true,
2547 help_template = DNS_HELP_TEMPLATE,
2548 after_help = DNS_ZONES_AFTER_HELP
2549)]
2550pub struct DnsZonesCmd {
2551 #[command(subcommand)]
2552 pub command: DnsZonesSubCommand,
2553}
2554
2555#[derive(Subcommand, Debug)]
2556pub enum DnsZonesSubCommand {
2557 #[command(about = "List zones for a provider account")]
2558 List(DnsZoneListCmd),
2559 #[command(about = "Fetch one zone by id")]
2560 Get(DnsZoneGetCmd),
2561 #[command(about = "Create a new zone")]
2562 Create(DnsZoneCreateCmd),
2563 #[command(about = "Edit zone-level properties")]
2564 Edit(DnsZoneEditCmd),
2565 #[command(about = "Delete a zone")]
2566 Delete(DnsZoneDeleteCmd),
2567}
2568
2569#[derive(Args, Debug)]
2570pub struct DnsZoneListCmd {
2571 #[arg(long, value_enum, default_value = "cloudflare")]
2572 pub provider: DnsProviderKind,
2573 #[arg(long)]
2574 pub account_id: Option<String>,
2575 #[arg(long)]
2576 pub account_name: Option<String>,
2577 #[arg(long = "account-name-op")]
2578 pub account_name_op: Option<String>,
2579 #[arg(long)]
2580 pub name: Option<String>,
2581 #[arg(long = "name-op")]
2582 pub name_op: Option<String>,
2583 #[arg(long)]
2584 pub status: Option<String>,
2585 #[arg(long = "type", value_delimiter = ',')]
2586 pub zone_types: Vec<String>,
2587 #[arg(long)]
2588 pub r#match: Option<String>,
2589 #[arg(long)]
2590 pub order: Option<String>,
2591 #[arg(long)]
2592 pub direction: Option<String>,
2593 #[arg(long)]
2594 pub page: Option<u64>,
2595 #[arg(long = "per-page")]
2596 pub per_page: Option<u64>,
2597 #[arg(long)]
2598 pub token: Option<String>,
2599}
2600
2601#[derive(Args, Debug)]
2602pub struct DnsZoneGetCmd {
2603 #[arg(long, value_enum, default_value = "cloudflare")]
2604 pub provider: DnsProviderKind,
2605 #[arg(long)]
2606 pub zone_id: String,
2607 #[arg(long)]
2608 pub token: Option<String>,
2609}
2610
2611#[derive(Args, Debug)]
2612pub struct DnsZoneCreateCmd {
2613 #[arg(long, value_enum, default_value = "cloudflare")]
2614 pub provider: DnsProviderKind,
2615 #[arg(long)]
2616 pub name: String,
2617 #[arg(long)]
2618 pub account_id: Option<String>,
2619 #[arg(long)]
2620 pub jump_start: bool,
2621 #[arg(long = "type")]
2622 pub zone_type: Option<String>,
2623 #[arg(long)]
2624 pub token: Option<String>,
2625}
2626
2627#[derive(Args, Debug)]
2628pub struct DnsZoneEditCmd {
2629 #[arg(long, value_enum, default_value = "cloudflare")]
2630 pub provider: DnsProviderKind,
2631 #[arg(long)]
2632 pub zone_id: String,
2633 #[arg(long)]
2634 pub paused: Option<bool>,
2635 #[arg(long = "type")]
2636 pub zone_type: Option<String>,
2637 #[arg(long = "vanity-name-server")]
2638 pub vanity_name_servers: Vec<String>,
2639 #[arg(long)]
2640 pub token: Option<String>,
2641}
2642
2643#[derive(Args, Debug)]
2644pub struct DnsZoneDeleteCmd {
2645 #[arg(long, value_enum, default_value = "cloudflare")]
2646 pub provider: DnsProviderKind,
2647 #[arg(long)]
2648 pub zone_id: String,
2649 #[arg(long)]
2650 pub token: Option<String>,
2651}
2652
2653#[derive(Args, Debug)]
2654#[command(
2655 about = "List, create, edit, import, export, and batch DNS records",
2656 arg_required_else_help = true,
2657 help_template = DNS_HELP_TEMPLATE,
2658 after_help = DNS_RECORDS_AFTER_HELP
2659)]
2660pub struct DnsRecordsCmd {
2661 #[command(subcommand)]
2662 pub command: DnsRecordsSubCommand,
2663}
2664
2665#[derive(Subcommand, Debug)]
2666pub enum DnsRecordsSubCommand {
2667 #[command(about = "List records in a zone")]
2668 List(DnsRecordListCmd),
2669 #[command(about = "Fetch one record by id")]
2670 Get(DnsRecordGetCmd),
2671 #[command(about = "Create a new DNS record")]
2672 Create(DnsRecordCreateCmd),
2673 #[command(about = "Replace a record by id with a full payload")]
2674 Replace(DnsRecordReplaceCmd),
2675 #[command(about = "Patch selected fields on a record")]
2676 Edit(DnsRecordEditCmd),
2677 #[command(about = "Delete a record")]
2678 Delete(DnsRecordDeleteCmd),
2679 #[command(about = "Apply a Cloudflare batch record payload from JSON")]
2680 Batch(DnsRecordBatchCmd),
2681 #[command(about = "Import a BIND-style zone file into a zone")]
2682 Import(DnsRecordImportCmd),
2683 #[command(about = "Export a zone as a BIND-style zone file")]
2684 Export(DnsRecordExportCmd),
2685}
2686
2687#[derive(Args, Debug)]
2688pub struct DnsRecordListCmd {
2689 #[arg(long, value_enum, default_value = "cloudflare")]
2690 pub provider: DnsProviderKind,
2691 #[arg(long)]
2692 pub zone_id: String,
2693 #[arg(long = "type")]
2694 pub record_type: Option<String>,
2695 #[arg(long)]
2696 pub name: Option<String>,
2697 #[arg(long)]
2698 pub page: Option<u64>,
2699 #[arg(long = "per-page")]
2700 pub per_page: Option<u64>,
2701 #[arg(long)]
2702 pub token: Option<String>,
2703}
2704
2705#[derive(Args, Debug)]
2706pub struct DnsRecordGetCmd {
2707 #[arg(long, value_enum, default_value = "cloudflare")]
2708 pub provider: DnsProviderKind,
2709 #[arg(long)]
2710 pub zone_id: String,
2711 #[arg(long)]
2712 pub record_id: String,
2713 #[arg(long)]
2714 pub token: Option<String>,
2715}
2716
2717#[derive(Args, Debug)]
2718pub struct DnsRecordCreateCmd {
2719 #[arg(long, value_enum, default_value = "cloudflare")]
2720 pub provider: DnsProviderKind,
2721 #[arg(long)]
2722 pub zone_id: String,
2723 #[arg(long = "type")]
2724 pub record_type: String,
2725 #[arg(long)]
2726 pub name: String,
2727 #[arg(long)]
2728 pub content: String,
2729 #[arg(long)]
2730 pub ttl: Option<u32>,
2731 #[arg(long)]
2732 pub proxied: Option<bool>,
2733 #[arg(long)]
2734 pub priority: Option<u32>,
2735 #[arg(long)]
2736 pub comment: Option<String>,
2737 #[arg(long = "tag")]
2738 pub tags: Vec<String>,
2739 #[arg(long = "data-json")]
2740 pub data_json: Option<String>,
2741 #[arg(long = "settings-json")]
2742 pub settings_json: Option<String>,
2743 #[arg(long)]
2744 pub token: Option<String>,
2745}
2746
2747#[derive(Args, Debug)]
2748pub struct DnsRecordReplaceCmd {
2749 #[command(flatten)]
2750 pub common: DnsRecordCreateCmd,
2751 #[arg(long)]
2752 pub record_id: String,
2753}
2754
2755#[derive(Args, Debug)]
2756pub struct DnsRecordEditCmd {
2757 #[arg(long, value_enum, default_value = "cloudflare")]
2758 pub provider: DnsProviderKind,
2759 #[arg(long)]
2760 pub zone_id: String,
2761 #[arg(long)]
2762 pub record_id: String,
2763 #[arg(long = "type")]
2764 pub record_type: Option<String>,
2765 #[arg(long)]
2766 pub name: Option<String>,
2767 #[arg(long)]
2768 pub content: Option<String>,
2769 #[arg(long)]
2770 pub ttl: Option<u32>,
2771 #[arg(long)]
2772 pub proxied: Option<bool>,
2773 #[arg(long)]
2774 pub priority: Option<u32>,
2775 #[arg(long)]
2776 pub comment: Option<String>,
2777 #[arg(long = "tag")]
2778 pub tags: Vec<String>,
2779 #[arg(long = "data-json")]
2780 pub data_json: Option<String>,
2781 #[arg(long = "settings-json")]
2782 pub settings_json: Option<String>,
2783 #[arg(long)]
2784 pub token: Option<String>,
2785}
2786
2787#[derive(Args, Debug)]
2788pub struct DnsRecordDeleteCmd {
2789 #[arg(long, value_enum, default_value = "cloudflare")]
2790 pub provider: DnsProviderKind,
2791 #[arg(long)]
2792 pub zone_id: String,
2793 #[arg(long)]
2794 pub record_id: String,
2795 #[arg(long)]
2796 pub token: Option<String>,
2797}
2798
2799#[derive(Args, Debug)]
2800pub struct DnsRecordBatchCmd {
2801 #[arg(long, value_enum, default_value = "cloudflare")]
2802 pub provider: DnsProviderKind,
2803 #[arg(long)]
2804 pub zone_id: String,
2805 #[arg(long)]
2806 pub input: PathBuf,
2807 #[arg(long)]
2808 pub token: Option<String>,
2809}
2810
2811#[derive(Args, Debug)]
2812pub struct DnsRecordImportCmd {
2813 #[arg(long, value_enum, default_value = "cloudflare")]
2814 pub provider: DnsProviderKind,
2815 #[arg(long)]
2816 pub zone_id: String,
2817 #[arg(long)]
2818 pub file: PathBuf,
2819 #[arg(long)]
2820 pub token: Option<String>,
2821}
2822
2823#[derive(Args, Debug)]
2824pub struct DnsRecordExportCmd {
2825 #[arg(long, value_enum, default_value = "cloudflare")]
2826 pub provider: DnsProviderKind,
2827 #[arg(long)]
2828 pub zone_id: String,
2829 #[arg(long)]
2830 pub output: Option<PathBuf>,
2831 #[arg(long)]
2832 pub token: Option<String>,
2833}
2834
2835#[derive(Args, Debug)]
2836#[command(
2837 about = "Inspect or edit DNSSEC status for a zone",
2838 arg_required_else_help = true,
2839 help_template = DNS_HELP_TEMPLATE,
2840 after_help = DNS_DNSSEC_AFTER_HELP
2841)]
2842pub struct DnssecCmd {
2843 #[command(subcommand)]
2844 pub command: DnssecSubCommand,
2845}
2846
2847#[derive(Subcommand, Debug)]
2848pub enum DnssecSubCommand {
2849 #[command(about = "Fetch DNSSEC state for a zone")]
2850 Get(DnssecGetCmd),
2851 #[command(about = "Edit DNSSEC-related flags for a zone")]
2852 Edit(DnssecEditCmd),
2853}
2854
2855#[derive(Args, Debug)]
2856pub struct DnssecGetCmd {
2857 #[arg(long, value_enum, default_value = "cloudflare")]
2858 pub provider: DnsProviderKind,
2859 #[arg(long)]
2860 pub zone_id: String,
2861 #[arg(long)]
2862 pub token: Option<String>,
2863}
2864
2865#[derive(Args, Debug)]
2866pub struct DnssecEditCmd {
2867 #[arg(long, value_enum, default_value = "cloudflare")]
2868 pub provider: DnsProviderKind,
2869 #[arg(long)]
2870 pub zone_id: String,
2871 #[arg(long)]
2872 pub status: Option<String>,
2873 #[arg(long = "dnssec-multi-signer")]
2874 pub dnssec_multi_signer: Option<bool>,
2875 #[arg(long = "dnssec-presigned")]
2876 pub dnssec_presigned: Option<bool>,
2877 #[arg(long = "dnssec-use-nsec3")]
2878 pub dnssec_use_nsec3: Option<bool>,
2879 #[arg(long)]
2880 pub token: Option<String>,
2881}
2882
2883#[derive(Args, Debug)]
2884#[command(
2885 about = "Inspect or edit provider DNS settings for a zone",
2886 arg_required_else_help = true,
2887 help_template = DNS_HELP_TEMPLATE,
2888 after_help = DNS_SETTINGS_AFTER_HELP
2889)]
2890pub struct DnsSettingsCmd {
2891 #[command(subcommand)]
2892 pub command: DnsSettingsSubCommand,
2893}
2894
2895#[derive(Subcommand, Debug)]
2896pub enum DnsSettingsSubCommand {
2897 #[command(about = "Fetch provider DNS settings for a zone")]
2898 Get(DnsSettingsGetCmd),
2899 #[command(about = "Edit provider DNS settings for a zone")]
2900 Edit(DnsSettingsEditCmd),
2901}
2902
2903#[derive(Args, Debug)]
2904pub struct DnsSettingsGetCmd {
2905 #[arg(long, value_enum, default_value = "cloudflare")]
2906 pub provider: DnsProviderKind,
2907 #[arg(long)]
2908 pub zone_id: String,
2909 #[arg(long)]
2910 pub token: Option<String>,
2911}
2912
2913#[derive(Args, Debug)]
2914pub struct DnsSettingsEditCmd {
2915 #[arg(long, value_enum, default_value = "cloudflare")]
2916 pub provider: DnsProviderKind,
2917 #[arg(long)]
2918 pub zone_id: String,
2919 #[arg(long)]
2920 pub flatten_all_cnames: Option<bool>,
2921 #[arg(long)]
2922 pub foundation_dns: Option<bool>,
2923 #[arg(long)]
2924 pub multi_provider: Option<bool>,
2925 #[arg(long)]
2926 pub ns_ttl: Option<u32>,
2927 #[arg(long)]
2928 pub secondary_overrides: Option<bool>,
2929 #[arg(long)]
2930 pub zone_mode: Option<String>,
2931 #[arg(long = "reference-zone-id")]
2932 pub reference_zone_id: Option<String>,
2933 #[arg(long = "nameservers-type")]
2934 pub nameservers_type: Option<String>,
2935 #[arg(long = "nameservers-ns-set")]
2936 pub nameservers_ns_set: Option<u32>,
2937 #[arg(long = "soa-json")]
2938 pub soa_json: Option<String>,
2939 #[arg(long)]
2940 pub token: Option<String>,
2941}
2942
2943#[derive(Args, Debug)]
2944#[command(
2945 arg_required_else_help = true,
2946 disable_help_subcommand = true,
2947 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2948 after_help = crate::cli::help_render::DOMAINS_AFTER_HELP
2949)]
2950pub struct DomainsCmd {
2951 #[arg(long, value_enum, default_value = "cloudflare")]
2952 pub provider: DomainsProviderKind,
2953 #[arg(long)]
2954 pub account_id: Option<String>,
2955 #[arg(long)]
2956 pub token: Option<String>,
2957 #[command(subcommand)]
2958 pub command: DomainsSubCommand,
2959}
2960
2961#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
2962pub enum DomainsProviderKind {
2963 Cloudflare,
2964}
2965
2966#[derive(Subcommand, Debug)]
2967pub enum DomainsSubCommand {
2968 Search(DomainsSearchCmd),
2969 Check(DomainsCheckCmd),
2970 List(DomainsListCmd),
2971}
2972
2973#[derive(Args, Debug)]
2974pub struct DomainsSearchCmd {
2975 #[arg(long)]
2976 pub query: String,
2977 #[arg(long = "extension")]
2978 pub extensions: Vec<String>,
2979 #[arg(long)]
2980 pub limit: Option<usize>,
2981}
2982
2983#[derive(Args, Debug)]
2984pub struct DomainsCheckCmd {
2985 #[arg(long = "domain", required = true)]
2986 pub domains: Vec<String>,
2987}
2988
2989#[derive(Args, Debug)]
2990pub struct DomainsListCmd {}
2991
2992#[derive(Args, Debug)]
2993pub struct GenerateSystemdCmd {
2994 #[arg(
2995 long,
2996 default_value = "/etc/systemd/system",
2997 help = "Directory where the systemd units are written"
2998 )]
2999 pub output_dir: PathBuf,
3000 #[arg(long, help = "Only generate the unit for this service name")]
3001 pub service: Option<String>,
3002 #[arg(
3003 long,
3004 default_value_t = true,
3005 help = "Also generate the xbp-api systemd unit alongside project/services"
3006 )]
3007 pub api: bool,
3008}
3009
3010#[derive(Args, Debug)]
3011#[command(
3012 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3013 after_help = crate::cli::help_render::DONE_AFTER_HELP
3014)]
3015pub struct DoneCmd {
3016 #[arg(long, help = "Root directory under which to discover git repos")]
3017 pub root: Option<std::path::PathBuf>,
3018 #[arg(
3019 long,
3020 default_value = "24 hours ago",
3021 help = "Git --since value (e.g. '7 days ago')"
3022 )]
3023 pub since: String,
3024 #[arg(short, long, help = "Output Markdown file path")]
3025 pub output: Option<std::path::PathBuf>,
3026 #[arg(long, help = "Skip AI summarization (OpenRouter)")]
3027 pub no_ai: bool,
3028 #[arg(short, long, help = "Discover repos recursively")]
3029 pub recursive: bool,
3030 #[arg(long, help = "Exclude repo by name (repeatable)")]
3031 pub exclude: Vec<String>,
3032}
3033
3034#[derive(Args, Debug)]
3035pub struct FixProcessMonitorJsonCmd {
3036 #[arg(help = "Path to a Cursor process-monitor JSON export")]
3037 pub path: std::path::PathBuf,
3038 #[arg(
3039 long,
3040 help = "Check whether the file needs repair without writing changes"
3041 )]
3042 pub check: bool,
3043 #[arg(
3044 long,
3045 help = "Print repaired JSON to stdout instead of overwriting the file"
3046 )]
3047 pub stdout: bool,
3048}
3049
3050#[derive(Args, Debug)]
3051pub struct CursorCmd {
3052 #[command(subcommand)]
3053 pub command: CursorSubCommand,
3054}
3055
3056#[derive(Subcommand, Debug)]
3057pub enum CursorSubCommand {
3058 #[command(about = "Upload local Cursor file history to the XBP dashboard")]
3059 Ingest {
3060 #[arg(
3061 long,
3062 help = "Scan local Cursor history without uploading to the dashboard"
3063 )]
3064 dry_run: bool,
3065 },
3066}
3067
3068#[cfg(feature = "nordvpn")]
3069#[derive(Args, Debug)]
3070pub struct NordvpnCmd {
3071 #[arg(
3072 trailing_var_arg = true,
3073 allow_hyphen_values = true,
3074 help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
3075 )]
3076 pub args: Vec<String>,
3077}
3078
3079#[cfg(feature = "kubernetes")]
3080#[derive(Args, Debug)]
3081pub struct KubernetesCmd {
3082 #[command(subcommand)]
3083 pub command: KubernetesSubCommand,
3084}
3085
3086#[cfg(feature = "kubernetes")]
3087#[derive(Args, Debug)]
3088pub struct KubernetesAddonCmd {
3089 #[command(subcommand)]
3090 pub command: KubernetesAddonSubCommand,
3091}
3092
3093#[cfg(feature = "kubernetes")]
3094#[derive(Subcommand, Debug)]
3095pub enum KubernetesAddonSubCommand {
3096 List,
3098 Enable {
3100 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
3101 name: String,
3102 },
3103 Disable {
3105 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
3106 name: String,
3107 },
3108}
3109
3110#[cfg(feature = "kubernetes")]
3111#[derive(Subcommand, Debug)]
3112pub enum KubernetesSubCommand {
3113 Check {
3115 #[arg(long, help = "Kubeconfig context to target")]
3116 context: Option<String>,
3117 #[arg(
3118 long,
3119 default_value = "default",
3120 help = "Namespace to probe for workload readiness"
3121 )]
3122 namespace: String,
3123 #[arg(long, help = "Skip live cluster calls (tooling check only)")]
3124 offline: bool,
3125 },
3126 Generate {
3128 #[arg(long, help = "Logical app name (used for resource names)")]
3129 name: String,
3130 #[arg(long, help = "Container image reference")]
3131 image: String,
3132 #[arg(long, default_value_t = 80, help = "Container port for the service")]
3133 port: u16,
3134 #[arg(long, default_value_t = 1, help = "Replica count")]
3135 replicas: u16,
3136 #[arg(
3137 long,
3138 default_value = "default",
3139 help = "Namespace for generated resources"
3140 )]
3141 namespace: String,
3142 #[arg(
3143 long,
3144 default_value = "k8s/xbp-manifest.yaml",
3145 help = "Path to write the manifest bundle"
3146 )]
3147 output: String,
3148 #[arg(long, help = "Optional ingress host (creates Ingress when set)")]
3149 host: Option<String>,
3150 },
3151 Apply {
3153 #[arg(long, help = "Path to manifest file")]
3154 file: String,
3155 #[arg(long, help = "Override kube context")]
3156 context: Option<String>,
3157 #[arg(long, help = "Override namespace")]
3158 namespace: Option<String>,
3159 #[arg(long, help = "Use --dry-run=server")]
3160 dry_run: bool,
3161 },
3162 Status {
3164 #[arg(long, default_value = "default", help = "Namespace to summarize")]
3165 namespace: String,
3166 #[arg(long, help = "Override kube context")]
3167 context: Option<String>,
3168 },
3169 Addons(KubernetesAddonCmd),
3171 DashboardToken {
3173 #[arg(
3174 long,
3175 default_value = "kube-system",
3176 help = "Namespace containing the dashboard token secret"
3177 )]
3178 namespace: String,
3179 #[arg(
3180 long,
3181 default_value = "microk8s-dashboard-token",
3182 help = "Secret name containing the dashboard login token"
3183 )]
3184 secret: String,
3185 #[arg(long, help = "Override kube context")]
3186 context: Option<String>,
3187 },
3188 ObservabilityCreds {
3190 #[arg(
3191 long,
3192 default_value = "observability",
3193 help = "Namespace containing Grafana secret"
3194 )]
3195 namespace: String,
3196 #[arg(
3197 long,
3198 default_value = "kube-prom-stack-grafana",
3199 help = "Grafana secret name"
3200 )]
3201 secret: String,
3202 #[arg(long, help = "Override kube context")]
3203 context: Option<String>,
3204 },
3205 Issuer {
3207 #[arg(
3208 long,
3209 help = "Email used for Let's Encrypt account registration (required)"
3210 )]
3211 email: String,
3212 #[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
3213 name: String,
3214 #[arg(
3215 long,
3216 default_value = "default",
3217 help = "Namespace for the Issuer resource"
3218 )]
3219 namespace: String,
3220 #[arg(
3221 long,
3222 default_value = "https://acme-v02.api.letsencrypt.org/directory",
3223 help = "ACME server URL (production by default)"
3224 )]
3225 server: String,
3226 #[arg(
3227 long,
3228 default_value = "letsencrypt-account-key",
3229 help = "Secret used to store the ACME account private key"
3230 )]
3231 private_key_secret: String,
3232 #[arg(
3233 long,
3234 default_value = "nginx",
3235 help = "Ingress class name used for HTTP01 solving"
3236 )]
3237 ingress_class_name: String,
3238 #[arg(long, help = "Override kube context")]
3239 context: Option<String>,
3240 #[arg(long, help = "Use --dry-run=server")]
3241 dry_run: bool,
3242 },
3243}
3244
3245#[cfg(test)]
3246mod tests {
3247 use super::{
3248 Cli, CloudflareConfigAction, CloudflaredSubCommand, Commands, DnsProviderKind,
3249 DnsSubCommand, DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand,
3250 GenerateSubCommand, LinearConfigAction, NetworkFloatingIpSubCommand,
3251 NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand, NetworkSubCommand, SshCmd,
3252 };
3253 #[cfg(feature = "secrets")]
3254 use super::{
3255 CloudflareSecretsSubCommand, SecretsProviderKind, SecretsStoresSubCommand,
3256 SecretsSubCommand,
3257 };
3258 use clap::Parser;
3259 use std::path::PathBuf;
3260
3261 #[test]
3262 fn parses_network_floating_ip_add() {
3263 let cli = Cli::parse_from([
3264 "xbp",
3265 "network",
3266 "floating-ip",
3267 "add",
3268 "--ip",
3269 "1.2.3.4",
3270 "--apply",
3271 ]);
3272
3273 match cli.command {
3274 Some(Commands::Network(network)) => match network.command {
3275 NetworkSubCommand::FloatingIp(fip) => match fip.command {
3276 NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
3277 assert_eq!(ip, "1.2.3.4");
3278 assert!(apply);
3279 }
3280 _ => panic!("expected add subcommand"),
3281 },
3282 _ => panic!("expected floating-ip subcommand"),
3283 },
3284 _ => panic!("expected network command"),
3285 }
3286 }
3287
3288 #[test]
3289 fn parses_generate_config_update() {
3290 let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
3291
3292 match cli.command {
3293 Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
3294 GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
3295 _ => panic!("expected generate config command"),
3296 },
3297 _ => panic!("expected generate command"),
3298 }
3299 }
3300
3301 #[test]
3302 fn parses_commit_command_with_dry_run() {
3303 let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
3304
3305 match cli.command {
3306 Some(Commands::Commit(commit_cmd)) => {
3307 assert!(commit_cmd.dry_run);
3308 assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
3309 assert_eq!(commit_cmd.model, None);
3310 }
3311 _ => panic!("expected commit command"),
3312 }
3313 }
3314
3315 #[test]
3316 fn parses_linear_select_initiative_config_command() {
3317 let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
3318
3319 match cli.command {
3320 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
3321 Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
3322 assert!(matches!(
3323 linear_cmd.action,
3324 LinearConfigAction::SelectInitiative
3325 ));
3326 }
3327 _ => panic!("expected linear config provider"),
3328 },
3329 _ => panic!("expected config command"),
3330 }
3331 }
3332
3333 #[test]
3334 fn parses_ssh_command_with_cloudflared_and_key_auth() {
3335 let cli = Cli::parse_from([
3336 "xbp",
3337 "ssh",
3338 "--host",
3339 "ssh.internal",
3340 "--username",
3341 "deploy",
3342 "--private-key",
3343 "C:/Users/floris/.ssh/id_ed25519",
3344 "--cloudflared-hostname",
3345 "bastion.example.com",
3346 "--command",
3347 "htop",
3348 ]);
3349
3350 let Some(Commands::Ssh(SshCmd {
3351 ssh_host,
3352 ssh_username,
3353 private_key,
3354 cloudflared_hostname,
3355 command,
3356 ..
3357 })) = cli.command
3358 else {
3359 panic!("expected shell command");
3360 };
3361
3362 assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
3363 assert_eq!(ssh_username.as_deref(), Some("deploy"));
3364 assert_eq!(
3365 private_key,
3366 Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
3367 );
3368 assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
3369 assert_eq!(command.as_deref(), Some("htop"));
3370 }
3371
3372 #[test]
3373 fn parses_cloudflared_tcp_command() {
3374 let cli = Cli::parse_from([
3375 "xbp",
3376 "cloudflared",
3377 "tcp",
3378 "--hostname",
3379 "bastion.example.com",
3380 "--listener",
3381 "127.0.0.1:2222",
3382 ]);
3383
3384 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
3385 panic!("expected cloudflared command");
3386 };
3387
3388 match cloudflared_cmd.command {
3389 CloudflaredSubCommand::Tcp(tcp_cmd) => {
3390 assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
3391 assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
3392 }
3393 }
3394 }
3395
3396 #[test]
3397 fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
3398 let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
3399
3400 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
3401 panic!("expected cloudflared command");
3402 };
3403
3404 match cloudflared_cmd.command {
3405 CloudflaredSubCommand::Tcp(tcp_cmd) => {
3406 assert_eq!(tcp_cmd.hostname, None);
3407 assert_eq!(tcp_cmd.listener, None);
3408 }
3409 }
3410 }
3411
3412 #[test]
3413 fn parses_version_workspace_publish_run_command() {
3414 let cli = Cli::parse_from([
3415 "xbp",
3416 "version",
3417 "workspace",
3418 "publish",
3419 "run",
3420 "--repo",
3421 "C:/Users/floris/Documents/GitHub/athena",
3422 "--dry-run",
3423 "--from",
3424 "athena-s3",
3425 ]);
3426
3427 let Some(Commands::Version(version_cmd)) = cli.command else {
3428 panic!("expected version command");
3429 };
3430
3431 match version_cmd.command {
3432 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
3433 match workspace_cmd.command {
3434 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
3435 match publish_cmd.command {
3436 super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
3437 assert_eq!(
3438 run_cmd.target.repo,
3439 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
3440 );
3441 assert!(!run_cmd.target.json);
3442 assert!(run_cmd.dry_run);
3443 assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
3444 }
3445 _ => panic!("expected publish run"),
3446 }
3447 }
3448 _ => panic!("expected workspace publish"),
3449 }
3450 }
3451 _ => panic!("expected version workspace command"),
3452 }
3453 }
3454
3455 #[test]
3456 fn parses_version_workspace_publish_plan_with_only_and_include_prereqs() {
3457 let cli = Cli::parse_from([
3458 "xbp",
3459 "version",
3460 "workspace",
3461 "publish",
3462 "plan",
3463 "--repo",
3464 "C:/Users/floris/Documents/GitHub/athena-auth",
3465 "--only",
3466 "athena-auth",
3467 "--include-prereqs",
3468 ]);
3469
3470 let Some(Commands::Version(version_cmd)) = cli.command else {
3471 panic!("expected version command");
3472 };
3473
3474 match version_cmd.command {
3475 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
3476 match workspace_cmd.command {
3477 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
3478 match publish_cmd.command {
3479 super::VersionWorkspacePublishSubCommand::Plan(plan_cmd) => {
3480 assert_eq!(
3481 plan_cmd.target.repo,
3482 Some(PathBuf::from(
3483 "C:/Users/floris/Documents/GitHub/athena-auth"
3484 ))
3485 );
3486 assert_eq!(plan_cmd.only.as_deref(), Some("athena-auth"));
3487 assert!(plan_cmd.include_prereqs);
3488 }
3489 _ => panic!("expected publish plan"),
3490 }
3491 }
3492 _ => panic!("expected workspace publish"),
3493 }
3494 }
3495 _ => panic!("expected version workspace command"),
3496 }
3497 }
3498
3499 #[test]
3500 fn parses_commit_alias_with_push_flag() {
3501 let cli = Cli::parse_from(["xbp", "c", "-p"]);
3502
3503 let Some(Commands::Commit(commit_cmd)) = cli.command else {
3504 panic!("expected commit command");
3505 };
3506
3507 assert!(commit_cmd.push);
3508 assert!(!commit_cmd.dry_run);
3509 }
3510
3511 #[test]
3512 fn parses_version_alias_release_alias() {
3513 let cli = Cli::parse_from(["xbp", "v", "r", "--draft", "--publish", "--force"]);
3514
3515 let Some(Commands::Version(version_cmd)) = cli.command else {
3516 panic!("expected version command");
3517 };
3518
3519 let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
3520 panic!("expected release subcommand");
3521 };
3522
3523 assert!(release_cmd.draft);
3524 assert!(release_cmd.publish);
3525 assert!(release_cmd.force);
3526 }
3527
3528 #[test]
3529 fn parses_publish_command_target_filter() {
3530 let cli = Cli::parse_from([
3531 "xbp",
3532 "publish",
3533 "--allow-dirty",
3534 "--force",
3535 "--include-prereqs",
3536 "--target",
3537 "npm",
3538 "--manifest-path",
3539 "apps/web/package.json",
3540 ]);
3541
3542 let Some(Commands::Publish(publish_cmd)) = cli.command else {
3543 panic!("expected publish command");
3544 };
3545
3546 assert!(publish_cmd.allow_dirty);
3547 assert!(publish_cmd.force);
3548 assert!(publish_cmd.include_prereqs);
3549 assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
3550 assert_eq!(
3551 publish_cmd.manifest_path,
3552 Some(PathBuf::from("apps/web/package.json"))
3553 );
3554 }
3555
3556 #[test]
3557 fn parses_npm_setup_release_config_command() {
3558 let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
3559
3560 let Some(Commands::Config(config_cmd)) = cli.command else {
3561 panic!("expected config command");
3562 };
3563 let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
3564 panic!("expected npm config command");
3565 };
3566
3567 assert!(matches!(
3568 registry_cmd.action,
3569 super::RegistryConfigAction::SetupRelease
3570 ));
3571 }
3572
3573 #[test]
3574 fn parses_crates_login_config_command() {
3575 let cli = Cli::parse_from(["xbp", "config", "crates", "login"]);
3576
3577 let Some(Commands::Config(config_cmd)) = cli.command else {
3578 panic!("expected config command");
3579 };
3580 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
3581 panic!("expected crates config command");
3582 };
3583
3584 assert!(matches!(
3585 crates_cmd.action,
3586 super::CratesConfigAction::Login { .. }
3587 ));
3588 }
3589
3590 #[test]
3591 fn parses_crates_logout_config_command() {
3592 let cli = Cli::parse_from(["xbp", "config", "crates", "logout"]);
3593
3594 let Some(Commands::Config(config_cmd)) = cli.command else {
3595 panic!("expected config command");
3596 };
3597 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
3598 panic!("expected crates config command");
3599 };
3600
3601 assert!(matches!(
3602 crates_cmd.action,
3603 super::CratesConfigAction::Logout
3604 ));
3605 }
3606
3607 #[test]
3608 fn parses_shell_alias_as_ssh_command() {
3609 let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
3610
3611 let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
3612 panic!("expected ssh command through shell alias");
3613 };
3614
3615 assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
3616 }
3617
3618 #[test]
3619 fn parses_api_request_command() {
3620 let cli = Cli::parse_from([
3621 "xbp",
3622 "api",
3623 "request",
3624 "/api/registry/installers/python-pip",
3625 "--web",
3626 "--method",
3627 "GET",
3628 "--header",
3629 "accept: application/json",
3630 ]);
3631
3632 let Some(Commands::Api(api_cmd)) = cli.command else {
3633 panic!("expected api command");
3634 };
3635
3636 match api_cmd.command {
3637 super::ApiSubCommand::Request(request_cmd) => {
3638 assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
3639 assert!(request_cmd.target.web);
3640 assert_eq!(request_cmd.method.as_deref(), Some("GET"));
3641 assert_eq!(
3642 request_cmd.target.header,
3643 vec!["accept: application/json".to_string()]
3644 );
3645 }
3646 _ => panic!("expected api request subcommand"),
3647 }
3648 }
3649
3650 #[test]
3651 fn parses_api_projects_list_command() {
3652 let cli = Cli::parse_from([
3653 "xbp",
3654 "api",
3655 "projects",
3656 "list",
3657 "--organization-id",
3658 "org_123",
3659 ]);
3660
3661 let Some(Commands::Api(api_cmd)) = cli.command else {
3662 panic!("expected api command");
3663 };
3664
3665 match api_cmd.command {
3666 super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
3667 super::ApiProjectsSubCommand::List(list_cmd) => {
3668 assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
3669 }
3670 _ => panic!("expected projects list subcommand"),
3671 },
3672 _ => panic!("expected projects subcommand"),
3673 }
3674 }
3675
3676 #[test]
3677 fn parses_api_routes_create_command() {
3678 let cli = Cli::parse_from([
3679 "xbp",
3680 "api",
3681 "routes",
3682 "create",
3683 "--domain",
3684 "demo.local",
3685 "--target",
3686 "http://127.0.0.1:3000",
3687 "--weighted-target",
3688 "http://127.0.0.1:3001=3",
3689 "--base-url",
3690 "http://127.0.0.1:8080",
3691 ]);
3692
3693 let Some(Commands::Api(api_cmd)) = cli.command else {
3694 panic!("expected api command");
3695 };
3696
3697 match api_cmd.command {
3698 super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
3699 super::ApiRoutesSubCommand::Create(create_cmd) => {
3700 assert_eq!(create_cmd.domain, "demo.local");
3701 assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
3702 assert_eq!(
3703 create_cmd.weighted_target,
3704 vec!["http://127.0.0.1:3001=3".to_string()]
3705 );
3706 assert_eq!(
3707 create_cmd.target_options.base_url.as_deref(),
3708 Some("http://127.0.0.1:8080")
3709 );
3710 }
3711 _ => panic!("expected routes create subcommand"),
3712 },
3713 _ => panic!("expected routes subcommand"),
3714 }
3715 }
3716
3717 #[test]
3718 fn parses_hetzner_vswitch_setup_command() {
3719 let cli = Cli::parse_from([
3720 "xbp",
3721 "network",
3722 "hetzner",
3723 "vswitch",
3724 "setup",
3725 "--ip",
3726 "10.0.3.2",
3727 "--vlan-id",
3728 "4000",
3729 "--interface",
3730 "enp0s31f6",
3731 "--apply",
3732 ]);
3733
3734 let Some(Commands::Network(network_cmd)) = cli.command else {
3735 panic!("expected network command");
3736 };
3737
3738 match network_cmd.command {
3739 NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
3740 NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
3741 NetworkHetznerVswitchSubCommand::Setup {
3742 ip,
3743 cidr,
3744 interface,
3745 vlan_id,
3746 apply,
3747 ..
3748 } => {
3749 assert_eq!(ip, "10.0.3.2");
3750 assert_eq!(cidr, 24);
3751 assert_eq!(interface.as_deref(), Some("enp0s31f6"));
3752 assert_eq!(vlan_id, 4000);
3753 assert!(apply);
3754 }
3755 },
3756 },
3757 _ => panic!("expected hetzner subcommand"),
3758 }
3759 }
3760
3761 #[cfg(feature = "secrets")]
3762 #[test]
3763 fn parses_secrets_diag_command() {
3764 let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
3765
3766 match cli.command {
3767 Some(Commands::Secrets(secrets_cmd)) => {
3768 assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
3769 assert_eq!(secrets_cmd.environment, "xbp-dev");
3770 }
3771 _ => panic!("expected secrets command"),
3772 }
3773 }
3774
3775 #[cfg(feature = "secrets")]
3776 #[test]
3777 fn parses_secrets_environment_override() {
3778 let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
3779
3780 match cli.command {
3781 Some(Commands::Secrets(secrets_cmd)) => {
3782 assert_eq!(secrets_cmd.environment, "xbp-prod");
3783 assert!(matches!(
3784 secrets_cmd.command,
3785 Some(SecretsSubCommand::Push(_))
3786 ));
3787 }
3788 _ => panic!("expected secrets command"),
3789 }
3790 }
3791
3792 #[test]
3793 fn parses_version_discover_command() {
3794 let cli = Cli::parse_from(["xbp", "version", "discover", "--dry-run"]);
3795
3796 match cli.command {
3797 Some(Commands::Version(version_cmd)) => match version_cmd.command {
3798 Some(super::VersionSubCommand::Discover(discover_cmd)) => {
3799 assert!(discover_cmd.dry_run);
3800 assert!(!discover_cmd.no_register);
3801 }
3802 _ => panic!("expected version discover subcommand"),
3803 },
3804 _ => panic!("expected version command"),
3805 }
3806 }
3807
3808 #[cfg(feature = "secrets")]
3809 #[test]
3810 fn parses_secrets_providers_command() {
3811 let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
3812
3813 match cli.command {
3814 Some(Commands::Secrets(secrets_cmd)) => {
3815 assert!(matches!(
3816 secrets_cmd.command,
3817 Some(SecretsSubCommand::Providers)
3818 ));
3819 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
3820 }
3821 _ => panic!("expected secrets command"),
3822 }
3823 }
3824
3825 #[cfg(feature = "secrets")]
3826 #[test]
3827 fn parses_cloudflare_secret_store_create() {
3828 let cli = Cli::parse_from([
3829 "xbp",
3830 "secrets",
3831 "--provider",
3832 "cloudflare",
3833 "stores",
3834 "create",
3835 "--name",
3836 "prod",
3837 ]);
3838
3839 match cli.command {
3840 Some(Commands::Secrets(secrets_cmd)) => {
3841 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
3842 match secrets_cmd.command {
3843 Some(SecretsSubCommand::Stores(stores_cmd)) => {
3844 assert!(matches!(
3845 stores_cmd.command,
3846 SecretsStoresSubCommand::Create(_)
3847 ));
3848 }
3849 _ => panic!("expected stores subcommand"),
3850 }
3851 }
3852 _ => panic!("expected secrets command"),
3853 }
3854 }
3855
3856 #[cfg(feature = "secrets")]
3857 #[test]
3858 fn parses_cloudflare_secret_duplicate() {
3859 let cli = Cli::parse_from([
3860 "xbp",
3861 "secrets",
3862 "--provider",
3863 "cloudflare",
3864 "secrets",
3865 "duplicate",
3866 "--store-id",
3867 "store_1",
3868 "--secret-id",
3869 "secret_1",
3870 "--name",
3871 "COPY",
3872 ]);
3873
3874 match cli.command {
3875 Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
3876 Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
3877 assert!(matches!(
3878 secrets_cmd.command,
3879 CloudflareSecretsSubCommand::Duplicate(_)
3880 ));
3881 }
3882 _ => panic!("expected cloudflare secrets subcommand"),
3883 },
3884 _ => panic!("expected secrets command"),
3885 }
3886 }
3887
3888 #[test]
3889 fn parses_workers_secret_put_from_stdin_command() {
3890 let cli = Cli::parse_from([
3891 "xbp",
3892 "workers",
3893 "secrets",
3894 "--environment",
3895 "production",
3896 "put",
3897 "--name",
3898 "API_KEY",
3899 "--from-stdin",
3900 ]);
3901
3902 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3903 panic!("expected workers command");
3904 };
3905
3906 match workers_cmd.command {
3907 super::WorkersSubCommand::Secrets(secrets_cmd) => {
3908 assert_eq!(
3909 secrets_cmd.target.environment.as_deref(),
3910 Some("production")
3911 );
3912 match secrets_cmd.command {
3913 super::WorkersSecretsSubCommand::Put(put_cmd) => {
3914 assert_eq!(put_cmd.name, "API_KEY");
3915 assert!(put_cmd.from_stdin);
3916 assert_eq!(put_cmd.value, None);
3917 }
3918 _ => panic!("expected workers secret put"),
3919 }
3920 }
3921 _ => panic!("expected workers secrets command"),
3922 }
3923 }
3924
3925 #[test]
3926 fn parses_workers_d1_migrations_local_command() {
3927 let cli = Cli::parse_from([
3928 "xbp",
3929 "workers",
3930 "d1",
3931 "migrations",
3932 "apply",
3933 "DB",
3934 "--local",
3935 "--environment",
3936 "preview",
3937 ]);
3938
3939 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3940 panic!("expected workers command");
3941 };
3942
3943 match workers_cmd.command {
3944 super::WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
3945 super::WorkersD1SubCommand::Migrations(migrations_cmd) => {
3946 match migrations_cmd.command {
3947 super::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
3948 assert_eq!(apply_cmd.database, "DB");
3949 assert!(apply_cmd.local);
3950 assert!(!apply_cmd.remote);
3951 assert_eq!(apply_cmd.target.environment.as_deref(), Some("preview"));
3952 }
3953 }
3954 }
3955 },
3956 _ => panic!("expected workers d1 command"),
3957 }
3958 }
3959
3960 #[test]
3961 fn parses_workers_deploy_ci_version_upload_command() {
3962 let cli = Cli::parse_from(["xbp", "workers", "deploy", "ci", "--version-upload"]);
3963
3964 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3965 panic!("expected workers command");
3966 };
3967
3968 match workers_cmd.command {
3969 super::WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
3970 super::WorkersDeploySubCommand::Ci(ci_cmd) => {
3971 assert!(ci_cmd.version_upload);
3972 }
3973 _ => panic!("expected workers deploy ci command"),
3974 },
3975 _ => panic!("expected workers deploy command"),
3976 }
3977 }
3978
3979 #[test]
3980 fn parses_workers_list_alias_command() {
3981 let cli = Cli::parse_from(["xbp", "workers", "ls", "--all"]);
3982
3983 let Some(Commands::Workers(workers_cmd)) = cli.command else {
3984 panic!("expected workers command");
3985 };
3986
3987 match workers_cmd.command {
3988 super::WorkersSubCommand::List(list_cmd) => {
3989 assert!(list_cmd.all);
3990 assert!(!list_cmd.json);
3991 }
3992 _ => panic!("expected workers list command"),
3993 }
3994 }
3995
3996 #[test]
3997 fn parses_workers_logs_follow_and_build_flags() {
3998 let cli = Cli::parse_from([
3999 "xbp",
4000 "workers",
4001 "logs",
4002 "-f",
4003 "--build",
4004 "--failed",
4005 "xbp-production",
4006 ]);
4007
4008 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4009 panic!("expected workers command");
4010 };
4011
4012 match workers_cmd.command {
4013 super::WorkersSubCommand::Logs(logs_cmd) => {
4014 assert!(logs_cmd.follow);
4015 assert!(logs_cmd.build);
4016 assert!(logs_cmd.failed);
4017 assert_eq!(logs_cmd.script_name.as_deref(), Some("xbp-production"));
4018 }
4019 _ => panic!("expected workers logs command"),
4020 }
4021 }
4022
4023 #[test]
4024 fn parses_workers_logs_worker_and_wait_flags() {
4025 let cli = Cli::parse_from([
4026 "xbp",
4027 "workers",
4028 "logs",
4029 "--worker",
4030 "suits-formations",
4031 "--json",
4032 "--wait",
4033 "--wait-seconds",
4034 "60",
4035 ]);
4036
4037 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4038 panic!("expected workers command");
4039 };
4040
4041 match workers_cmd.command {
4042 super::WorkersSubCommand::Logs(logs_cmd) => {
4043 assert_eq!(
4044 logs_cmd.target.worker.as_deref(),
4045 Some("suits-formations")
4046 );
4047 assert!(logs_cmd.json);
4048 assert!(logs_cmd.wait);
4049 assert_eq!(logs_cmd.wait_seconds, 60);
4050 }
4051 _ => panic!("expected workers logs command"),
4052 }
4053 }
4054
4055 #[test]
4056 fn parses_workers_logs_output_and_filter_flags() {
4057 let cli = Cli::parse_from([
4058 "xbp",
4059 "workers",
4060 "logs",
4061 "--build",
4062 "-n",
4063 "200",
4064 "-o",
4065 "build.log",
4066 "-g",
4067 "error",
4068 "--errors-only",
4069 "--list-builds",
4070 "--build-index",
4071 "1",
4072 ]);
4073
4074 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4075 panic!("expected workers command");
4076 };
4077
4078 match workers_cmd.command {
4079 super::WorkersSubCommand::Logs(logs_cmd) => {
4080 assert!(logs_cmd.build);
4081 assert_eq!(logs_cmd.lines, Some(200));
4082 assert_eq!(
4083 logs_cmd.output.as_deref().and_then(|path| path.to_str()),
4084 Some("build.log")
4085 );
4086 assert_eq!(logs_cmd.grep.as_deref(), Some("error"));
4087 assert!(logs_cmd.errors_only);
4088 assert!(logs_cmd.list_builds);
4089 assert_eq!(logs_cmd.build_index, Some(1));
4090 }
4091 _ => panic!("expected workers logs command"),
4092 }
4093 }
4094
4095 #[test]
4096 fn parses_workers_list_filter_and_limit_flags() {
4097 let cli = Cli::parse_from([
4098 "xbp",
4099 "workers",
4100 "list",
4101 "--failed",
4102 "-n",
4103 "5",
4104 "--sort",
4105 "modified",
4106 ]);
4107
4108 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4109 panic!("expected workers command");
4110 };
4111
4112 match workers_cmd.command {
4113 super::WorkersSubCommand::List(list_cmd) => {
4114 assert!(list_cmd.failed);
4115 assert_eq!(list_cmd.limit, Some(5));
4116 assert_eq!(list_cmd.sort, "modified");
4117 }
4118 _ => panic!("expected workers list command"),
4119 }
4120 }
4121
4122 #[test]
4123 fn parses_worker_alias_command() {
4124 let cli = Cli::parse_from(["xbp", "worker", "env", "--show-values"]);
4125
4126 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4127 panic!("expected workers command through alias");
4128 };
4129
4130 match workers_cmd.command {
4131 super::WorkersSubCommand::Env(env_cmd) => {
4132 assert!(env_cmd.show_values);
4133 }
4134 _ => panic!("expected workers env command"),
4135 }
4136 }
4137
4138 #[test]
4139 fn parses_dns_providers_command() {
4140 let cli = Cli::parse_from(["xbp", "dns", "providers"]);
4141
4142 match cli.command {
4143 Some(Commands::Dns(dns_cmd)) => {
4144 assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
4145 }
4146 _ => panic!("expected dns command"),
4147 }
4148 }
4149
4150 #[test]
4151 fn dns_zone_list_defaults_provider_to_cloudflare() {
4152 let cli = Cli::parse_from(["xbp", "dns", "zones", "list"]);
4153
4154 let Some(Commands::Dns(dns_cmd)) = cli.command else {
4155 panic!("expected dns command");
4156 };
4157
4158 match dns_cmd.command {
4159 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
4160 DnsZonesSubCommand::List(list_cmd) => {
4161 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4162 }
4163 _ => panic!("expected zones list command"),
4164 },
4165 _ => panic!("expected zones command"),
4166 }
4167 }
4168
4169 #[test]
4170 fn dns_record_list_defaults_provider_to_cloudflare() {
4171 let cli = Cli::parse_from(["xbp", "dns", "records", "list", "--zone-id", "zone_123"]);
4172
4173 let Some(Commands::Dns(dns_cmd)) = cli.command else {
4174 panic!("expected dns command");
4175 };
4176
4177 match dns_cmd.command {
4178 DnsSubCommand::Records(records_cmd) => match records_cmd.command {
4179 super::DnsRecordsSubCommand::List(list_cmd) => {
4180 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4181 assert_eq!(list_cmd.zone_id, "zone_123");
4182 }
4183 _ => panic!("expected records list command"),
4184 },
4185 _ => panic!("expected records command"),
4186 }
4187 }
4188
4189 #[test]
4190 fn dns_help_includes_descriptions_and_examples() {
4191 let err = Cli::try_parse_from(["xbp", "dns", "-h"]).expect_err("help");
4192 let rendered = err.to_string();
4193
4194 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
4195 assert!(rendered.contains("Manage DNS providers, zones, records, DNSSEC, and settings"));
4196 assert!(rendered.contains("List supported DNS providers and current implementation status"));
4197 assert!(rendered.contains("xbp dns records create"));
4198 }
4199
4200 #[test]
4201 fn dns_providers_help_includes_discovery_note() {
4202 let err = Cli::try_parse_from(["xbp", "dns", "providers", "-h"]).expect_err("help");
4203 let rendered = err.to_string();
4204
4205 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
4206 assert!(rendered.contains("Implemented providers are wired into `xbp dns` today."));
4207 }
4208
4209 #[test]
4210 fn dns_records_without_subcommand_displays_help_screen() {
4211 let err = Cli::try_parse_from(["xbp", "dns", "records"]).expect_err("missing subcommand");
4212 let rendered = err.to_string();
4213
4214 assert!(matches!(
4215 err.kind(),
4216 clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
4217 | clap::error::ErrorKind::MissingSubcommand
4218 ));
4219 assert!(rendered.contains("List, create, edit, import, export, and batch DNS records"));
4220 assert!(rendered.contains("Create a new DNS record"));
4221 assert!(rendered.contains("xbp dns records import"));
4222 }
4223
4224 #[test]
4225 fn parses_dns_zone_list_command() {
4226 let cli = Cli::parse_from([
4227 "xbp",
4228 "dns",
4229 "zones",
4230 "list",
4231 "--provider",
4232 "cloudflare",
4233 "--account-name-op",
4234 "contains",
4235 "--type",
4236 "full,partial",
4237 ]);
4238
4239 match cli.command {
4240 Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
4241 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
4242 DnsZonesSubCommand::List(list_cmd) => {
4243 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4244 assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
4245 assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
4246 }
4247 _ => panic!("expected dns zones list"),
4248 },
4249 _ => panic!("expected dns zones"),
4250 },
4251 _ => panic!("expected dns command"),
4252 }
4253 }
4254
4255 #[test]
4256 fn parses_domains_search_command() {
4257 let cli = Cli::parse_from([
4258 "xbp",
4259 "domains",
4260 "search",
4261 "--query",
4262 "xbp",
4263 "--extension",
4264 "com",
4265 ]);
4266
4267 match cli.command {
4268 Some(Commands::Domains(domains_cmd)) => {
4269 assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
4270 assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
4271 }
4272 _ => panic!("expected domains command"),
4273 }
4274 }
4275
4276 #[test]
4277 fn parses_cloudflare_config_account_id_command() {
4278 let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
4279
4280 match cli.command {
4281 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
4282 Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
4283 assert!(matches!(
4284 cloudflare_cmd.action,
4285 Some(CloudflareConfigAction::SetAccountId { .. })
4286 ));
4287 }
4288 _ => panic!("expected cloudflare config provider"),
4289 },
4290 _ => panic!("expected config command"),
4291 }
4292 }
4293}