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