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