1use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Parser, Debug)]
6#[command(
7 name = "xbp",
8 version,
9 disable_version_flag = true,
11 about = "Deploy, operate, and debug services with one CLI.",
12 long_about = "XBP is an operations-first CLI for deployments, diagnostics, service orchestration,\nnetwork controls, and runtime observability.",
13 disable_help_subcommand = false,
14 next_line_help = true,
15 help_template = crate::cli::help_render::XBP_ROOT_HELP_TEMPLATE,
16 after_help = crate::cli::help_render::XBP_ROOT_AFTER_HELP
17)]
18pub struct Cli {
19 #[arg(
21 short = 'v',
23 long = "version",
24 visible_short_alias = 'V',
25 action = ArgAction::Version
26 )]
27 print_version: Option<bool>,
28 #[arg(long, global = true, help = "Enable verbose debugging output")]
29 pub debug: bool,
30 #[arg(short = 'l', help = "List pm2 processes")]
31 pub list: bool,
32 #[arg(short = 'p', long = "port", help = "Filter by port number")]
33 pub port: Option<u16>,
34 #[arg(long, help = "Open logs directory")]
35 pub logs: bool,
36 #[arg(long, help = "Print the complete alphabetical command reference")]
37 pub commands: bool,
38
39 #[command(subcommand)]
40 pub command: Option<Commands>,
41}
42
43#[derive(Subcommand, Debug)]
44pub enum Commands {
45 #[command(about = "Inspect or manage listening ports")]
46 Ports(PortsCmd),
47 #[command(
48 about = "Analyze the current git worktree and create a conventional commit",
49 visible_alias = "c"
50 )]
51 Commit(CommitCmd),
52 #[command(about = "Initialize an XBP project in the current directory")]
53 Init,
54 #[command(about = "Install common dependencies for host setup")]
55 Setup,
56 #[command(about = "Redeploy one service or the entire project")]
57 Redeploy {
58 #[arg(
59 help = "Service name to redeploy (optional, uses legacy redeploy.sh if not provided)"
60 )]
61 service_name: Option<String>,
62 },
63 #[command(about = "Run the legacy remote redeploy workflow over SSH")]
64 RedeployV2(RedeployV2Cmd),
65 #[command(about = "Inspect project/global config and manage provider keys")]
66 Config(ConfigCmd),
67 #[command(
68 about = "Install supported host packages or project tooling",
69 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
70 after_help = crate::commands::INSTALL_COMMAND_AFTER_HELP
71 )]
72 Install {
73 #[arg(short = 'l', long = "list", help = "List installable targets and exit")]
74 list: bool,
75 #[arg(
76 long = "force",
77 help = "Allow an explicitly selected install method to run even when it does not match the current OS"
78 )]
79 force: bool,
80 #[arg(help = "Install target (leave empty to show installable options)")]
81 package: Option<String>,
82 },
83 #[command(about = "Tail local or remote logs")]
84 Logs(LogsCmd),
85 #[command(
86 about = "Open an interactive remote shell over SSH",
87 visible_alias = "shell"
88 )]
89 Ssh(SshCmd),
90 #[command(about = "Open or manage cloudflared TCP forwarders")]
91 Cloudflared(CloudflaredCmd),
92 #[command(about = "List PM2 processes")]
93 List,
94 #[command(about = "Fetch an HTTP endpoint with sane defaults")]
95 Curl(CurlCmd),
96 #[command(about = "List configured services from project config")]
97 Services,
98 #[command(
99 about = "Run service commands with interactive preview/picker when no args given. Stores run history + registry under global ~/.xbp/logs/service/"
100 )]
101 Service {
102 #[arg(help = "Command (build/install/start/dev/pre). Omit to use interactive picker.")]
103 command: Option<String>,
104 #[arg(help = "Service name (omit for picker)")]
105 service_name: Option<String>,
106 },
107 #[command(about = "Manage NGINX site configs and upstream mappings")]
108 Nginx(NginxCmd),
109 #[command(about = "Manage host network configuration and floating IPs")]
110 Network(NetworkCmd),
111 #[command(about = "Run full system diagnostics and readiness checks")]
112 Diag(DiagCmd),
113 #[command(about = "Run health-check monitoring commands")]
114 Monitor(MonitorCmd),
115 #[command(about = "Capture a PM2 snapshot for later restore")]
116 Snapshot,
117 #[command(about = "Restore PM2 state from dump or latest snapshot")]
118 Resurrect,
119 #[command(about = "Stop a PM2 process by name or stop all")]
120 Stop {
121 #[arg(help = "PM2 process name or 'all' (default: all)")]
122 target: Option<String>,
123 },
124 #[command(about = "Flush PM2 logs globally or for a specific process")]
125 Flush {
126 #[arg(help = "Optional PM2 process name")]
127 target: Option<String>,
128 },
129 #[command(about = "Run or inspect the CLI login flow against the XBP dashboard")]
130 Login(LoginCmd),
131 #[command(about = "Show the current signed-in CLI identity")]
132 Whoami,
133 #[command(
134 about = "Inspect, reconcile, or bump project versions",
135 visible_alias = "v"
136 )]
137 Version(VersionCmd),
138 #[command(about = "Run configured npm/crates publish workflows for the current XBP project")]
139 Publish(PublishCmd),
140 #[command(about = "Show PM2 environment by name or numeric id")]
141 Env {
142 #[arg(help = "PM2 process name or id")]
143 target: String,
144 },
145 #[command(about = "Tail app logs or Kafka logs")]
146 Tail(TailCmd),
147 #[command(about = "Start a binary/process under PM2")]
148 Start {
149 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
150 args: Vec<String>,
151 },
152 #[command(about = "Generate helper artifacts such as systemd units")]
153 Generate(GenerateCmd),
154 #[cfg(feature = "secrets")]
155 #[command(about = "Manage env vars and GitHub Actions environment variables (feature-gated)")]
156 Secrets(SecretsCmd),
157 #[command(
158 about = "Manage Cloudflare Workers secrets, Wrangler config helpers, D1 migrations, and deploy flows",
159 visible_alias = "worker"
160 )]
161 Workers(WorkersCmd),
162 #[command(about = "Run canonical Cloudflare Worker + Container workflows for an XBP project")]
163 Cloudflare(CloudflareCmd),
164 #[command(about = "Manage DNS providers, zones, records, DNSSEC, and settings")]
165 Dns(DnsCmd),
166 #[command(about = "Discover and inspect registered domains")]
167 Domains(DomainsCmd),
168 #[command(
169 about = "Generate 'what did I get done' Markdown report from git commits across repos"
170 )]
171 Done(DoneCmd),
172 #[command(
173 about = "Repair malformed Cursor process-monitor JSON exports",
174 visible_alias = "fix-pm-json"
175 )]
176 FixProcessMonitorJson(FixProcessMonitorJsonCmd),
177 #[command(about = "Upload Cursor local file history to the XBP dashboard")]
178 Cursor(CursorCmd),
179 #[cfg(feature = "kubernetes")]
180 #[command(about = "Experimental Kubernetes cluster manager (feature-gated)")]
181 Kubernetes(KubernetesCmd),
182 #[cfg(feature = "nordvpn")]
183 #[command(about = "NordVPN meshnet setup and passthrough (feature-gated)")]
184 Nordvpn(NordvpnCmd),
185 #[cfg(feature = "monitoring")]
186 Monitoring(MonitoringCmd),
187 #[command(about = "Manage the XBP API server")]
188 Api(ApiCmd),
189 #[command(about = "Manage runner hosts, groups, inventory, and runner jobs")]
190 Runners(RunnersCmd),
191 #[command(about = "Built-in MCP server for AI agents (HTTP/SSE on port 1113)")]
192 Mcp(McpCmd),
193 #[command(
194 about = "Watch git worktree mutations, store local JSONL spools, and sync them to xbp.app",
195 visible_alias = "watch"
196 )]
197 WorktreeWatch(WorktreeWatchCmd),
198 #[cfg(feature = "docker")]
199 #[command(about = "Pass-through wrapper around the Docker CLI")]
200 Docker(DockerCmd),
201}
202
203pub fn command_label(command: &Commands) -> &'static str {
204 match command {
205 Commands::Ports(_) => "ports",
206 Commands::Commit(_) => "commit",
207 Commands::Init => "init",
208 Commands::Setup => "setup",
209 Commands::Redeploy { .. } => "redeploy",
210 Commands::RedeployV2(_) => "redeploy-v2",
211 Commands::Config(_) => "config",
212 Commands::Install { .. } => "install",
213 Commands::Logs(_) => "logs",
214 Commands::Ssh(_) => "ssh",
215 Commands::Cloudflared(_) => "cloudflared",
216 Commands::List => "list",
217 Commands::Curl(_) => "curl",
218 Commands::Services => "services",
219 Commands::Service { .. } => "service",
220 Commands::Nginx(_) => "nginx",
221 Commands::Network(_) => "network",
222 Commands::Diag(_) => "diag",
223 Commands::Monitor(_) => "monitor",
224 Commands::Snapshot => "snapshot",
225 Commands::Resurrect => "resurrect",
226 Commands::Stop { .. } => "stop",
227 Commands::Flush { .. } => "flush",
228 Commands::Login(_) => "login",
229 Commands::Whoami => "whoami",
230 Commands::Version(_) => "version",
231 Commands::Publish(_) => "publish",
232 Commands::Env { .. } => "env",
233 Commands::Tail(_) => "tail",
234 Commands::Start { .. } => "start",
235 Commands::Generate(_) => "generate",
236 #[cfg(feature = "secrets")]
237 Commands::Secrets(_) => "secrets",
238 Commands::Workers(_) => "workers",
239 Commands::Cloudflare(_) => "cloudflare",
240 Commands::Dns(_) => "dns",
241 Commands::Domains(_) => "domains",
242 Commands::Done(_) => "done",
243 Commands::FixProcessMonitorJson(_) => "fix-process-monitor-json",
244 Commands::Cursor(_) => "cursor",
245 #[cfg(feature = "kubernetes")]
246 Commands::Kubernetes(_) => "kubernetes",
247 #[cfg(feature = "nordvpn")]
248 Commands::Nordvpn(_) => "nordvpn",
249 #[cfg(feature = "monitoring")]
250 Commands::Monitoring(_) => "monitoring",
251 Commands::Api(_) => "api",
252 Commands::Runners(_) => "runners",
253 Commands::Mcp(_) => "mcp",
254 Commands::WorktreeWatch(_) => "worktree-watch",
255 #[cfg(feature = "docker")]
256 Commands::Docker(_) => "docker",
257 }
258}
259
260#[derive(Args, Debug)]
261#[command(
262 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
263 after_help = crate::cli::help_render::COMMIT_AFTER_HELP
264)]
265pub struct CommitCmd {
266 #[arg(
267 long,
268 help = "Generate and print the conventional commit message without creating a git commit"
269 )]
270 pub dry_run: bool,
271 #[arg(
272 short = 'p',
273 long,
274 help = "Push after committing, or push pending local commits when nothing new needs committing"
275 )]
276 pub push: bool,
277 #[arg(long, help = "Skip OpenRouter and use local heuristics only")]
278 pub no_ai: bool,
279 #[arg(
280 long,
281 help = "OpenRouter model override used for commit generation; otherwise XBP uses the global config default"
282 )]
283 pub model: Option<String>,
284 #[arg(
285 long,
286 help = "Force the conventional commit scope (for example: cli, api, docs)"
287 )]
288 pub scope: Option<String>,
289}
290
291#[derive(Args, Debug)]
292#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
293pub struct PortsCmd {
294 #[arg(short = 'p', long = "port")]
295 pub port: Option<u16>,
296 #[arg(
297 long = "sort",
298 default_value = "port",
299 value_parser = ["port", "pid"],
300 help = "Sort port rows by port or pid (default: port)"
301 )]
302 pub sort: String,
303 #[arg(long = "kill")]
304 pub kill: bool,
305 #[arg(short = 'n', long = "nginx")]
306 pub nginx: bool,
307 #[arg(
308 long = "full",
309 help = "Show one unified ports view (reconciled listeners + exposure + security flags)"
310 )]
311 pub full: bool,
312 #[arg(
313 long = "no-local",
314 help = "Exclude connections where LocalAddr equals RemoteAddr"
315 )]
316 pub no_local: bool,
317 #[arg(
318 long = "exposure",
319 help = "Diagnose external exposure per port (binding + firewall layer)"
320 )]
321 pub exposure: bool,
322}
323
324#[derive(Args, Debug)]
325#[command(
326 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
327 after_help = crate::cli::help_render::CONFIG_AFTER_HELP
328)]
329pub struct ConfigCmd {
330 #[arg(
331 long,
332 help = "Show the current project config instead of opening global XBP paths"
333 )]
334 pub project: bool,
335 #[arg(long, help = "Print global XBP paths without opening them")]
336 pub no_open: bool,
337 #[command(subcommand)]
338 pub provider: Option<ConfigProviderCmd>,
339}
340
341#[derive(Subcommand, Debug)]
342pub enum ConfigProviderCmd {
343 #[command(about = "Manage the OpenRouter API key used by AI-enabled commands")]
344 Openrouter(ConfigSecretCmd),
345 #[command(about = "Manage the GitHub OAuth2 token used for release automation")]
346 Github(ConfigSecretCmd),
347 #[command(
348 about = "Manage Cloudflare API credentials used by secrets, DNS, and domains (run without a subcommand for the interactive setup wizard)"
349 )]
350 Cloudflare(CloudflareConfigCmd),
351 #[command(
352 about = "Manage the Linear API key used for release-note issue linking and initiative publishing"
353 )]
354 Linear(LinearConfigCmd),
355 #[command(about = "Manage npm registry auth and guided npm publish config")]
356 Npm(RegistryConfigCmd),
357 #[command(about = "Manage crates.io auth and guided crate publish config")]
358 Crates(CratesConfigCmd),
359 #[command(about = "Manage guided release config in .xbp/xbp.yaml")]
360 Release(ReleaseConfigCmd),
361}
362
363#[derive(Args, Debug)]
364pub struct ConfigSecretCmd {
365 #[command(subcommand)]
366 pub action: ConfigSecretAction,
367}
368
369#[derive(Subcommand, Debug)]
370pub enum ConfigSecretAction {
371 #[command(about = "Set provider key (omit value to enter it securely)")]
372 SetKey {
373 #[arg(help = "Provider key/token value")]
374 key: Option<String>,
375 },
376 #[command(about = "Delete the stored provider key")]
377 DeleteKey,
378 #[command(about = "Show whether a key is configured (masked by default)")]
379 Show {
380 #[arg(long, help = "Print full key/token value (not masked)")]
381 raw: bool,
382 },
383}
384
385#[derive(Args, Debug)]
386pub struct CloudflareConfigCmd {
387 #[command(subcommand)]
388 pub action: Option<CloudflareConfigAction>,
389}
390
391#[derive(Subcommand, Debug)]
392pub enum CloudflareConfigAction {
393 #[command(about = "Set Cloudflare API token (omit value to enter it securely)")]
394 SetKey {
395 #[arg(help = "Cloudflare API token")]
396 key: Option<String>,
397 },
398 #[command(about = "Delete the stored Cloudflare API token")]
399 DeleteKey,
400 #[command(about = "Show whether a Cloudflare API token is configured")]
401 ShowKey {
402 #[arg(long, help = "Print full token value (not masked)")]
403 raw: bool,
404 },
405 #[command(about = "Set the default Cloudflare account ID")]
406 SetAccountId {
407 #[arg(help = "Cloudflare account ID")]
408 account_id: Option<String>,
409 },
410 #[command(about = "Delete the stored default Cloudflare account ID")]
411 DeleteAccountId,
412 #[command(about = "Show whether a Cloudflare account ID is configured")]
413 ShowAccountId {
414 #[arg(long, help = "Print full account ID value (not masked)")]
415 raw: bool,
416 },
417 #[command(about = "Interactive dashboard OAuth linking flow for Cloudflare credentials")]
418 Login,
419 #[command(about = "Show Cloudflare credential sources and readiness")]
420 Status,
421 #[command(about = "Run the interactive Cloudflare credential setup wizard")]
422 Setup,
423}
424
425#[derive(Args, Debug)]
426pub struct LinearConfigCmd {
427 #[command(subcommand)]
428 pub action: LinearConfigAction,
429}
430
431#[derive(Subcommand, Debug)]
432pub enum LinearConfigAction {
433 #[command(about = "Set Linear API key (omit value to enter it securely)")]
434 SetKey {
435 #[arg(help = "Linear API key/token value")]
436 key: Option<String>,
437 },
438 #[command(about = "Delete the stored Linear API key")]
439 DeleteKey,
440 #[command(about = "Show whether a Linear API key is configured (masked by default)")]
441 Show {
442 #[arg(long, help = "Print full key/token value (not masked)")]
443 raw: bool,
444 },
445 #[command(
446 name = "select-initiative",
447 about = "Pick a Linear initiative for the current repo and save it to .xbp/xbp.yaml"
448 )]
449 SelectInitiative,
450}
451
452#[derive(Args, Debug)]
453pub struct RegistryConfigCmd {
454 #[command(subcommand)]
455 pub action: RegistryConfigAction,
456}
457
458#[derive(Args, Debug)]
459pub struct CratesConfigCmd {
460 #[command(subcommand)]
461 pub action: CratesConfigAction,
462}
463
464#[derive(Args, Debug)]
465pub struct ReleaseConfigCmd {
466 #[command(subcommand)]
467 pub action: ReleaseConfigAction,
468}
469
470#[derive(Subcommand, Debug)]
471pub enum RegistryConfigAction {
472 #[command(about = "Set registry token/key (omit value to enter it securely)")]
473 SetKey {
474 #[arg(help = "Registry token value")]
475 key: Option<String>,
476 },
477 #[command(about = "Delete the stored registry token")]
478 DeleteKey,
479 #[command(about = "Show whether a registry token is configured (masked by default)")]
480 Show {
481 #[arg(long, help = "Print full token value (not masked)")]
482 raw: bool,
483 },
484 #[command(
485 name = "setup-release",
486 about = "Interactively configure project publish settings in .xbp/xbp.yaml"
487 )]
488 SetupRelease,
489}
490
491#[derive(Subcommand, Debug)]
492pub enum CratesConfigAction {
493 #[command(about = "Set crates.io token (omit value to enter it securely)")]
494 SetKey {
495 #[arg(help = "crates.io token value")]
496 key: Option<String>,
497 },
498 #[command(about = "Delete the stored crates.io token from global XBP config")]
499 DeleteKey,
500 #[command(about = "Show whether a crates.io token is configured (masked by default)")]
501 Show {
502 #[arg(long, help = "Print full token value (not masked)")]
503 raw: bool,
504 },
505 #[command(
506 name = "setup-release",
507 about = "Interactively configure project publish settings in .xbp/xbp.yaml"
508 )]
509 SetupRelease,
510 #[command(
511 about = "Run `cargo login` using the stored crates token and sync Cargo's local credentials file"
512 )]
513 Login {
514 #[arg(help = "Optional crates.io token value to save before logging in")]
515 key: Option<String>,
516 },
517 #[command(
518 about = "Run `cargo logout` to remove Cargo's local crates.io credentials while keeping XBP's stored token"
519 )]
520 Logout,
521}
522
523#[derive(Subcommand, Debug)]
524pub enum ReleaseConfigAction {
525 #[command(
526 name = "setup",
527 about = "Interactively configure release tag naming in .xbp/xbp.yaml"
528 )]
529 Setup,
530}
531
532#[derive(Args, Debug)]
533#[command(help_template = crate::cli::help_render::XBP_HELP_TEMPLATE)]
534pub struct CurlCmd {
535 #[arg(help = "URL or domain to fetch, e.g. example.com or https://example.com/api")]
536 pub url: Option<String>,
537 #[arg(long, help = "Disable the default 15 second timeout")]
538 pub no_timeout: bool,
539}
540
541#[derive(Args, Debug)]
542#[command(
543 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
544 after_help = crate::cli::help_render::LOGIN_AFTER_HELP
545)]
546pub struct LoginCmd {
547 #[command(subcommand)]
548 pub action: Option<LoginSubCommand>,
549}
550
551#[derive(Subcommand, Debug)]
552pub enum LoginSubCommand {
553 #[command(about = "Show whether the current CLI session is still valid")]
554 Status,
555 #[command(about = "Revoke the current CLI token and clear local login state")]
556 Logout,
557}
558
559#[derive(Args, Debug)]
560#[command(
561 subcommand_precedence_over_arg = true,
562 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
563 after_help = crate::cli::help_render::VERSION_AFTER_HELP
564)]
565pub struct VersionCmd {
566 #[arg(
567 help = "Show versions, bump with major/minor/patch, or set an explicit version like 1.2.3"
568 )]
569 pub target: Option<String>,
570 #[arg(
571 short = 'v',
572 long = "version",
573 help = "Explicit version target; equivalent to the positional version value and overrides it when both are provided"
574 )]
575 pub explicit_version: Option<String>,
576 #[arg(long, help = "Show normalized git tags from `git tag --list`")]
577 pub git: bool,
578 #[command(subcommand)]
579 pub command: Option<VersionSubCommand>,
580}
581
582#[derive(Subcommand, Debug)]
583pub enum VersionSubCommand {
584 #[command(
585 about = "Create and push a git tag for this version, then create a GitHub release",
586 visible_alias = "r"
587 )]
588 Release(VersionReleaseCmd),
589 #[command(
590 about = "Manage Rust workspace release/version drift, sync, validation, and publish flow",
591 arg_required_else_help = true
592 )]
593 Workspace(VersionWorkspaceCmd),
594 #[command(
595 about = "Manage explicit release-domain ownership, validation, sync, diagnosis, and guarded releases",
596 arg_required_else_help = true
597 )]
598 Domain(VersionDomainCmd),
599 #[command(name = "discover", alias = "register", alias = "register-services")]
601 Discover(VersionDiscoverServicesCmd),
602 #[command(
603 about = "Bump versions only for packages with uncommitted changes in the working tree"
604 )]
605 Bump(VersionBumpCmd),
606}
607
608#[derive(Args, Debug)]
609pub struct VersionBumpCmd {
610 #[arg(long, help = "Preview bump selections without writing version files")]
611 pub dry_run: bool,
612 #[arg(
613 long,
614 group = "bump_kind",
615 help = "Default bump kind for --all and interactive default selection"
616 )]
617 pub major: bool,
618 #[arg(
619 long,
620 group = "bump_kind",
621 help = "Default bump kind for --all and interactive default"
622 )]
623 pub minor: bool,
624 #[arg(
625 long,
626 group = "bump_kind",
627 help = "Default bump kind for --all and interactive default"
628 )]
629 pub patch: bool,
630 #[arg(
631 long,
632 help = "Bump every mutated package with the selected default kind without prompting"
633 )]
634 pub all: bool,
635}
636
637#[derive(Args, Debug)]
638pub struct VersionDiscoverServicesCmd {
639 #[arg(
640 long,
641 help = "Preview discovered nested XBP services without writing .xbp/xbp.yaml"
642 )]
643 pub dry_run: bool,
644 #[arg(
645 long,
646 help = "Discover nested services only; do not register them in the root config (opt out)"
647 )]
648 pub no_register: bool,
649}
650
651#[derive(Args, Debug)]
652pub struct VersionReleaseCmd {
653 #[arg(
654 long,
655 help = "Release this version instead of auto-detecting from tracked files"
656 )]
657 pub version: Option<String>,
658 #[arg(
659 long = "flag",
660 value_enum,
661 help = "Append build metadata to the release version: dev, stable, beta, alpha, nightly, or exp"
662 )]
663 pub flag: Option<VersionReleaseFlag>,
664 #[arg(
665 long,
666 help = "Allow releasing with uncommitted changes in the working tree"
667 )]
668 pub allow_dirty: bool,
669 #[arg(long, help = "Release title (defaults to <version> - <repo>)")]
670 pub title: Option<String>,
671 #[arg(long, help = "Release notes body (Markdown)")]
672 pub notes: Option<String>,
673 #[arg(long, help = "Read release notes body from a file")]
674 pub notes_file: Option<PathBuf>,
675 #[arg(long, help = "Create as draft release")]
676 pub draft: bool,
677 #[arg(long, help = "Mark release as pre-release")]
678 pub prerelease: bool,
679 #[arg(
680 long,
681 help = "Run configured npm/crates publish workflows before creating the GitHub release"
682 )]
683 pub publish: bool,
684 #[arg(
685 long,
686 requires = "publish",
687 help = "Skip configured publish preflight commands when `--publish` is enabled and allow a dirty working tree"
688 )]
689 pub force: bool,
690 #[arg(
691 long,
692 value_enum,
693 default_value_t = VersionReleaseLatest::Legacy,
694 help = "Control GitHub latest flag: true, false, or legacy"
695 )]
696 pub make_latest: VersionReleaseLatest,
697}
698
699#[derive(Copy, Clone, Debug, ValueEnum)]
700pub enum VersionReleaseLatest {
701 True,
702 False,
703 Legacy,
704}
705
706#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq)]
707pub enum VersionReleaseFlag {
708 Dev,
709 Stable,
710 Beta,
711 Alpha,
712 Nightly,
713 Exp,
714}
715
716impl VersionReleaseFlag {
717 pub fn as_str(self) -> &'static str {
718 match self {
719 Self::Dev => "dev",
720 Self::Stable => "stable",
721 Self::Beta => "beta",
722 Self::Alpha => "alpha",
723 Self::Nightly => "nightly",
724 Self::Exp => "exp",
725 }
726 }
727}
728
729#[derive(Args, Debug)]
730#[command(
731 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
732 after_help = crate::cli::help_render::PUBLISH_AFTER_HELP
733)]
734pub struct PublishCmd {
735 #[arg(
736 long,
737 help = "Validate and print what would publish without uploading packages"
738 )]
739 pub dry_run: bool,
740 #[arg(
741 long,
742 help = "Allow publish workflows to run with a dirty working tree"
743 )]
744 pub allow_dirty: bool,
745 #[arg(long, help = "Skip configured preflight commands and publish anyway")]
746 pub force: bool,
747 #[arg(
748 long,
749 help = "When publishing a crate workspace member, also publish missing internal prerequisites in dependency order"
750 )]
751 pub include_prereqs: bool,
752 #[arg(long, help = "Limit publishing to one target: npm or crates")]
753 pub target: Option<String>,
754 #[arg(
755 long,
756 help = "Publish the npm package or crate belonging to this configured service"
757 )]
758 pub service: Option<String>,
759 #[arg(
760 long,
761 help = "Limit publishing to the workflow whose manifest matches this path"
762 )]
763 pub manifest_path: Option<PathBuf>,
764}
765
766#[derive(Args, Debug)]
767#[command(
768 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
769 after_help = "Examples:\n xbp version workspace check --repo C:/Users/floris/Documents/GitHub/athena\n xbp version workspace sync --version 3.16.5\n xbp version workspace sync --version 3.16.5 --write\n xbp version workspace validate --cargo-check --package-dry-run\n xbp version workspace publish plan\n xbp version workspace publish plan --only athena-auth --include-prereqs\n xbp version workspace publish run --dry-run\n xbp version workspace publish run --from athena-s3\n xbp version workspace publish run --only athena-auth --include-prereqs"
770)]
771pub struct VersionWorkspaceCmd {
772 #[command(subcommand)]
773 pub command: VersionWorkspaceSubCommand,
774}
775
776#[derive(Args, Debug)]
777#[command(
778 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
779 after_help = "Examples:\n xbp version domain init --name auth --root services/auth --write\n xbp version domain doctor --domain auth\n xbp version domain sync --domain auth --check\n xbp version domain diagnose --domain auth --log build.log\n xbp version domain release --domain auth --patch --deploy"
780)]
781pub struct VersionDomainCmd {
782 #[command(subcommand)]
783 pub command: VersionDomainSubCommand,
784}
785
786#[derive(Subcommand, Debug)]
787pub enum VersionDomainSubCommand {
788 #[command(about = "Discover or scaffold a version-domain policy")]
789 Init(VersionDomainInitCmd),
790 #[command(about = "Validate release-domain ownership and configured surfaces")]
791 Doctor(VersionDomainDoctorCmd),
792 #[command(about = "Preview or apply version-domain surface synchronization")]
793 Sync(VersionDomainSyncCmd),
794 #[command(about = "Classify build logs for release-domain drift and unsafe alignments")]
795 Diagnose(VersionDomainDiagnoseCmd),
796 #[command(about = "Run a guarded domain version bump/set and optional deploy")]
797 Release(VersionDomainReleaseCmd),
798}
799
800#[derive(Args, Debug)]
801pub struct VersionDomainInitCmd {
802 #[arg(long, help = "Release domain name")]
803 pub name: String,
804 #[arg(long, help = "Domain root path relative to the XBP project root")]
805 pub root: PathBuf,
806 #[arg(long, help = "Persist the generated domain to .xbp/xbp.yaml")]
807 pub write: bool,
808}
809
810#[derive(Args, Debug)]
811pub struct VersionDomainDoctorCmd {
812 #[arg(long = "domain", help = "Release domain name")]
813 pub domain: String,
814}
815
816#[derive(Args, Debug)]
817pub struct VersionDomainSyncCmd {
818 #[arg(long = "domain", help = "Release domain name")]
819 pub domain: String,
820 #[arg(
821 long,
822 conflicts_with = "write",
823 help = "Validate only; do not write files"
824 )]
825 pub check: bool,
826 #[arg(long, conflicts_with = "check", help = "Write configured surfaces")]
827 pub write: bool,
828}
829
830#[derive(Args, Debug)]
831pub struct VersionDomainDiagnoseCmd {
832 #[arg(long = "domain", help = "Release domain name")]
833 pub domain: String,
834 #[arg(long, help = "Build or deploy log file to classify")]
835 pub log: Option<PathBuf>,
836}
837
838#[derive(Args, Debug)]
839#[command(group(
840 clap::ArgGroup::new("domain_release_version")
841 .required(true)
842 .args(["patch", "minor", "major", "version"])
843))]
844pub struct VersionDomainReleaseCmd {
845 #[arg(long = "domain", help = "Release domain name")]
846 pub domain: String,
847 #[arg(long, help = "Bump the domain patch version")]
848 pub patch: bool,
849 #[arg(long, help = "Bump the domain minor version")]
850 pub minor: bool,
851 #[arg(long, help = "Bump the domain major version")]
852 pub major: bool,
853 #[arg(long, help = "Set an explicit domain version")]
854 pub version: Option<String>,
855 #[arg(long, help = "Run the configured Cloudflare app release after sync")]
856 pub deploy: bool,
857 #[arg(long, help = "Allow an otherwise blocked major-version jump")]
858 pub allow_major_jump: bool,
859 #[arg(
860 long,
861 value_name = "DOMAIN",
862 help = "Explicitly record an allowed cross-domain version source"
863 )]
864 pub allow_cross_domain_version: Option<String>,
865}
866
867#[derive(Args, Debug, Clone, Default)]
868pub struct VersionWorkspaceTargetArgs {
869 #[arg(
870 long,
871 help = "Workspace repo root to inspect (defaults to current project root)"
872 )]
873 pub repo: Option<PathBuf>,
874 #[arg(long, help = "Emit machine-readable JSON output")]
875 pub json: bool,
876}
877
878#[derive(Subcommand, Debug)]
879pub enum VersionWorkspaceSubCommand {
880 #[command(about = "Detect workspace release drift and exit non-zero when mismatches exist")]
881 Check(VersionWorkspaceCheckCmd),
882 #[command(about = "Preview or apply workspace-wide version alignment")]
883 Sync(VersionWorkspaceSyncCmd),
884 #[command(about = "Run structural and optional cargo validation for workspace publishability")]
885 Validate(VersionWorkspaceValidateCmd),
886 #[command(about = "Plan or execute crates.io publishing for workspace packages")]
887 Publish(VersionWorkspacePublishCmd),
888}
889
890#[derive(Args, Debug)]
891pub struct VersionWorkspaceCheckCmd {
892 #[command(flatten)]
893 pub target: VersionWorkspaceTargetArgs,
894 #[arg(
895 long,
896 help = "Expected release version (defaults to the root package version)"
897 )]
898 pub version: Option<String>,
899}
900
901#[derive(Args, Debug)]
902pub struct VersionWorkspaceSyncCmd {
903 #[command(flatten)]
904 pub target: VersionWorkspaceTargetArgs,
905 #[arg(
906 long,
907 help = "Target release version (defaults to the root package version)"
908 )]
909 pub version: Option<String>,
910 #[arg(
911 long,
912 help = "Write changes to disk instead of previewing the sync plan"
913 )]
914 pub write: bool,
915}
916
917#[derive(Args, Debug)]
918pub struct VersionWorkspaceValidateCmd {
919 #[command(flatten)]
920 pub target: VersionWorkspaceTargetArgs,
921 #[arg(long, help = "Limit cargo validation to a single package name")]
922 pub package: Option<String>,
923 #[arg(long, help = "Run `cargo check -q` as part of validation")]
924 pub cargo_check: bool,
925 #[arg(
926 long,
927 help = "Run `cargo publish --dry-run --locked` for publishable packages"
928 )]
929 pub package_dry_run: bool,
930}
931
932#[derive(Args, Debug)]
933#[command(arg_required_else_help = true)]
934pub struct VersionWorkspacePublishCmd {
935 #[command(subcommand)]
936 pub command: VersionWorkspacePublishSubCommand,
937}
938
939#[derive(Subcommand, Debug)]
940pub enum VersionWorkspacePublishSubCommand {
941 #[command(about = "Show publish order, crates.io visibility, and blockers without publishing")]
942 Plan(VersionWorkspacePublishPlanCmd),
943 #[command(about = "Publish workspace packages in dependency order")]
944 Run(VersionWorkspacePublishRunCmd),
945}
946
947#[derive(Args, Debug)]
948pub struct VersionWorkspacePublishPlanCmd {
949 #[command(flatten)]
950 pub target: VersionWorkspaceTargetArgs,
951 #[arg(long, help = "Limit the plan to one package")]
952 pub only: Option<String>,
953 #[arg(
954 long,
955 help = "When planning a single package, also include missing internal prerequisites"
956 )]
957 pub include_prereqs: bool,
958}
959
960#[derive(Args, Debug)]
961pub struct VersionWorkspacePublishRunCmd {
962 #[command(flatten)]
963 pub target: VersionWorkspaceTargetArgs,
964 #[arg(long, help = "Preview publish actions without calling cargo publish")]
965 pub dry_run: bool,
966 #[arg(
967 long,
968 help = "Start publishing from this package in the computed order"
969 )]
970 pub from: Option<String>,
971 #[arg(long, help = "Publish only this package")]
972 pub only: Option<String>,
973 #[arg(
974 long,
975 help = "When publishing one package, also publish missing internal prerequisites in dependency order"
976 )]
977 pub include_prereqs: bool,
978 #[arg(long, help = "Continue publishing remaining packages after a failure")]
979 pub continue_on_error: bool,
980 #[arg(long, help = "Allow publishing from a dirty worktree")]
981 pub allow_dirty: bool,
982 #[arg(
983 long,
984 default_value_t = 180.0,
985 help = "How long to wait for each published version to become visible on crates.io"
986 )]
987 pub timeout_seconds: f64,
988 #[arg(
989 long,
990 default_value_t = 5.0,
991 help = "How often to poll crates.io for the just-published version"
992 )]
993 pub poll_interval_seconds: f64,
994}
995
996#[derive(Args, Debug)]
997pub struct RedeployV2Cmd {
998 #[arg(short = 'p', long = "password")]
999 pub password: Option<String>,
1000 #[arg(short = 'u', long = "username")]
1001 pub username: Option<String>,
1002 #[arg(short = 'h', long = "host")]
1003 pub host: Option<String>,
1004 #[arg(short = 'd', long = "project-dir")]
1005 pub project_dir: Option<String>,
1006}
1007
1008#[derive(Args, Debug)]
1009#[command(
1010 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1011 after_help = crate::cli::help_render::LOGS_AFTER_HELP
1012)]
1013pub struct LogsCmd {
1014 #[arg()]
1015 pub project: Option<String>,
1016 #[arg(long = "ssh-host", help = "SSH host to stream logs from")]
1017 pub ssh_host: Option<String>,
1018 #[arg(long = "ssh-username", help = "SSH username for remote host")]
1019 pub ssh_username: Option<String>,
1020 #[arg(long = "ssh-password", help = "SSH password for remote host")]
1021 pub ssh_password: Option<String>,
1022}
1023
1024#[derive(Args, Debug)]
1025#[command(
1026 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1027 after_help = crate::cli::help_render::SSH_AFTER_HELP
1028)]
1029pub struct SshCmd {
1030 #[arg(long = "host", alias = "ssh-host", help = "SSH host or IP address")]
1031 pub ssh_host: Option<String>,
1032 #[arg(
1033 long = "port",
1034 default_value_t = 22,
1035 help = "SSH port for direct connections"
1036 )]
1037 pub ssh_port: u16,
1038 #[arg(
1039 long = "username",
1040 alias = "ssh-username",
1041 help = "SSH username for the remote host"
1042 )]
1043 pub ssh_username: Option<String>,
1044 #[arg(
1045 long = "password",
1046 alias = "ssh-password",
1047 help = "SSH password (omit to use stored config or a secure prompt)"
1048 )]
1049 pub ssh_password: Option<String>,
1050 #[arg(
1051 long,
1052 help = "Path to a private key file to use instead of password auth"
1053 )]
1054 pub private_key: Option<PathBuf>,
1055 #[arg(long, help = "Passphrase for --private-key when required")]
1056 pub private_key_passphrase: Option<String>,
1057 #[arg(
1058 long,
1059 help = "Run this remote command in a PTY instead of opening the default login shell"
1060 )]
1061 pub command: Option<String>,
1062 #[arg(
1063 long,
1064 help = "TERM value sent to the server (default: TERM env var or xterm-256color)"
1065 )]
1066 pub term: Option<String>,
1067 #[arg(long, help = "Disable SSH host key verification")]
1068 pub no_host_key_check: bool,
1069 #[arg(
1070 long,
1071 help = "Pin the SSH host key as a base64 blob when using tunnels or first-connect flows"
1072 )]
1073 pub host_key: Option<String>,
1074 #[arg(
1075 long,
1076 help = "Path to a known_hosts file used for SSH host verification"
1077 )]
1078 pub known_hosts_file: Option<PathBuf>,
1079 #[arg(
1080 long,
1081 help = "Cloudflare Access hostname used to open a local cloudflared TCP forwarder"
1082 )]
1083 pub cloudflared_hostname: Option<String>,
1084 #[arg(long, help = "Override the cloudflared binary path")]
1085 pub cloudflared_binary: Option<PathBuf>,
1086 #[arg(
1087 long,
1088 help = "Optional destination host:port passed to cloudflared access tcp"
1089 )]
1090 pub cloudflared_destination: Option<String>,
1091}
1092
1093#[derive(Args, Debug)]
1094#[command(
1095 arg_required_else_help = true,
1096 disable_help_subcommand = true,
1097 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
1098)]
1099pub struct CloudflaredCmd {
1100 #[command(subcommand)]
1101 pub command: CloudflaredSubCommand,
1102}
1103
1104#[derive(Subcommand, Debug)]
1105pub enum CloudflaredSubCommand {
1106 #[command(about = "Start a local cloudflared Access TCP forwarder")]
1107 Tcp(CloudflaredTcpCmd),
1108}
1109
1110#[derive(Args, Debug)]
1111#[command(
1112 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1113 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"
1114)]
1115pub struct CloudflaredTcpCmd {
1116 #[arg(long, help = "Protected Cloudflare Access hostname")]
1117 pub hostname: Option<String>,
1118 #[arg(
1119 long,
1120 help = "Local listener address for the forwarder (default: auto-allocate 127.0.0.1:<port>)"
1121 )]
1122 pub listener: Option<String>,
1123 #[arg(
1124 long,
1125 help = "Optional destination host:port passed to cloudflared access tcp"
1126 )]
1127 pub destination: Option<String>,
1128 #[arg(long, help = "Override the cloudflared binary path")]
1129 pub binary: Option<PathBuf>,
1130}
1131
1132#[derive(Args, Debug)]
1133#[command(
1134 arg_required_else_help = true,
1135 disable_help_subcommand = true,
1136 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1137 after_help = crate::cli::help_render::NGINX_AFTER_HELP
1138)]
1139pub struct NginxCmd {
1140 #[command(subcommand)]
1141 pub command: NginxSubCommand,
1142}
1143
1144#[derive(Args, Debug)]
1145#[command(
1146 arg_required_else_help = true,
1147 disable_help_subcommand = true,
1148 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE
1149)]
1150pub struct NetworkCmd {
1151 #[command(subcommand)]
1152 pub command: NetworkSubCommand,
1153}
1154
1155#[derive(Subcommand, Debug)]
1156pub enum NetworkSubCommand {
1157 #[command(about = "Manage persistent floating IP configuration")]
1158 FloatingIp(NetworkFloatingIpCmd),
1159 #[command(about = "Inspect discovered network configuration sources")]
1160 Config(NetworkConfigCmd),
1161 #[command(about = "Manage Hetzner-specific Linux network configuration")]
1162 Hetzner(NetworkHetznerCmd),
1163}
1164
1165#[derive(Args, Debug)]
1166pub struct NetworkFloatingIpCmd {
1167 #[command(subcommand)]
1168 pub command: NetworkFloatingIpSubCommand,
1169}
1170
1171#[derive(Subcommand, Debug)]
1172pub enum NetworkFloatingIpSubCommand {
1173 #[command(about = "Add a persistent floating IP entry to detected network backend")]
1174 Add {
1175 #[arg(long, help = "Floating IP address (IPv4 or IPv6)")]
1176 ip: String,
1177 #[arg(long, help = "CIDR suffix (defaults: IPv4=32, IPv6=64)")]
1178 cidr: Option<u8>,
1179 #[arg(long, help = "Network interface override (auto-detected when omitted)")]
1180 interface: Option<String>,
1181 #[arg(long, help = "Optional label for backend metadata/file naming")]
1182 label: Option<String>,
1183 #[arg(long, help = "Apply network changes after writing config")]
1184 apply: bool,
1185 #[arg(long, help = "Preview computed changes without writing files")]
1186 dry_run: bool,
1187 },
1188 #[command(about = "List floating IPs from runtime and persisted network config")]
1189 List {
1190 #[arg(long, help = "Emit JSON output")]
1191 json: bool,
1192 },
1193}
1194
1195#[derive(Args, Debug)]
1196pub struct NetworkConfigCmd {
1197 #[command(subcommand)]
1198 pub command: NetworkConfigSubCommand,
1199}
1200
1201#[derive(Subcommand, Debug)]
1202pub enum NetworkConfigSubCommand {
1203 #[command(about = "List detected backend and configuration source files")]
1204 List {
1205 #[arg(long, help = "Emit JSON output")]
1206 json: bool,
1207 },
1208}
1209
1210#[derive(Args, Debug)]
1211pub struct NetworkHetznerCmd {
1212 #[command(subcommand)]
1213 pub command: NetworkHetznerSubCommand,
1214}
1215
1216#[derive(Subcommand, Debug)]
1217pub enum NetworkHetznerSubCommand {
1218 #[command(about = "Configure a Hetzner vSwitch VLAN interface persistently")]
1219 Vswitch(NetworkHetznerVswitchCmd),
1220}
1221
1222#[derive(Args, Debug)]
1223pub struct NetworkHetznerVswitchCmd {
1224 #[command(subcommand)]
1225 pub command: NetworkHetznerVswitchSubCommand,
1226}
1227
1228#[derive(Subcommand, Debug)]
1229pub enum NetworkHetznerVswitchSubCommand {
1230 #[command(about = "Write persistent Linux config for a Hetzner vSwitch VLAN interface")]
1231 Setup {
1232 #[arg(
1233 long,
1234 help = "Private IPv4 address to assign on the vSwitch VLAN interface"
1235 )]
1236 ip: String,
1237 #[arg(
1238 long,
1239 default_value_t = 24,
1240 help = "CIDR prefix for --ip (default: 24)"
1241 )]
1242 cidr: u8,
1243 #[arg(long, help = "Physical parent interface (auto-detected when omitted)")]
1244 interface: Option<String>,
1245 #[arg(long, help = "Hetzner vSwitch VLAN ID")]
1246 vlan_id: u16,
1247 #[arg(long, default_value_t = 1400, help = "Interface MTU (default: 1400)")]
1248 mtu: u16,
1249 #[arg(
1250 long,
1251 default_value = "10.0.3.1",
1252 help = "Gateway for the routed Hetzner cloud network"
1253 )]
1254 gateway: String,
1255 #[arg(
1256 long,
1257 default_value = "10.0.0.0/16",
1258 help = "Destination CIDR routed through the Hetzner vSwitch gateway"
1259 )]
1260 route_cidr: String,
1261 #[arg(long, help = "Apply or activate the new config immediately")]
1262 apply: bool,
1263 #[arg(long, help = "Preview file changes without writing them")]
1264 dry_run: bool,
1265 },
1266}
1267
1268#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
1269pub enum NginxDnsMode {
1270 Manual,
1271 Plugin,
1272}
1273
1274#[derive(Subcommand, Debug)]
1275pub enum NginxSubCommand {
1276 #[command(
1277 about = "Provision an HTTPS NGINX reverse proxy with Certbot",
1278 long_about = "Provision an NGINX reverse proxy, issue or reuse Let's Encrypt certificates,\n\
1279and write final HTTP->HTTPS redirect + TLS proxy config.\n\
1280\n\
1281Wildcard domains (for example *.example.com) require DNS-01 mode.\n\
1282Use --dns-mode manual for interactive TXT record prompts, or --dns-mode plugin\n\
1283with --dns-plugin and --dns-creds for non-interactive provider automation."
1284 )]
1285 Setup {
1286 #[arg(short, long, help = "Domain name (supports wildcard: *.example.com)")]
1287 domain: String,
1288 #[arg(short, long, help = "Port to proxy to")]
1289 port: u16,
1290 #[arg(
1291 short,
1292 long,
1293 help = "Email used for Let's Encrypt account registration"
1294 )]
1295 email: String,
1296 #[arg(
1297 long,
1298 value_enum,
1299 default_value_t = NginxDnsMode::Manual,
1300 help = "DNS challenge mode for wildcard certificates: manual or plugin"
1301 )]
1302 dns_mode: NginxDnsMode,
1303 #[arg(
1304 long,
1305 help = "Certbot DNS plugin name for --dns-mode plugin (for example: cloudflare)"
1306 )]
1307 dns_plugin: Option<String>,
1308 #[arg(
1309 long,
1310 help = "Path to DNS plugin credentials file for --dns-mode plugin"
1311 )]
1312 dns_creds: Option<PathBuf>,
1313 #[arg(
1314 long,
1315 default_value_t = true,
1316 action = clap::ArgAction::Set,
1317 value_parser = clap::builder::BoolishValueParser::new(),
1318 help = "For wildcard domains, also request the base domain certificate (true|false)"
1319 )]
1320 include_base: bool,
1321 },
1322 #[command(about = "List discovered NGINX sites with listen/upstream ports")]
1323 List,
1324 #[command(about = "Show full NGINX config for one domain or all domains")]
1325 Show {
1326 #[arg(help = "Optional domain name to inspect")]
1327 domain: Option<String>,
1328 },
1329 #[command(about = "Open an NGINX site config in your configured editor")]
1330 Edit {
1331 #[arg(help = "Domain name to edit")]
1332 domain: String,
1333 },
1334 #[command(about = "Update upstream port for an existing NGINX site")]
1335 Update {
1336 #[arg(short, long, help = "Domain name to update")]
1337 domain: String,
1338 #[arg(short, long, help = "New port to proxy to")]
1339 port: u16,
1340 },
1341}
1342
1343#[derive(Args, Debug)]
1344#[command(
1345 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1346 after_help = crate::cli::help_render::DIAG_AFTER_HELP
1347)]
1348pub struct DiagCmd {
1349 #[arg(long, help = "Check Nginx configuration")]
1350 pub nginx: bool,
1351 #[arg(long, hide = true)]
1352 pub refresh_system_inventory: bool,
1353 #[arg(
1354 long,
1355 help = "Refresh and print persisted machine inventory in the global XBP config"
1356 )]
1357 pub codetime: bool,
1358 #[arg(
1359 long,
1360 help = "Inspect Cursor roaming data on Windows; implies --codetime"
1361 )]
1362 pub cursor: bool,
1363 #[arg(long, help = "Check specific ports (comma-separated)")]
1364 pub ports: Option<String>,
1365 #[arg(long, help = "Skip internet speed test")]
1366 pub no_speed_test: bool,
1367 #[arg(
1368 long,
1369 help = "Path to docker compose file to validate (defaults to docker-compose.yml/compose.yml)"
1370 )]
1371 pub compose_file: Option<String>,
1372}
1373
1374#[derive(Args, Debug)]
1375pub struct MonitorCmd {
1376 #[command(subcommand)]
1377 pub command: Option<MonitorSubCommand>,
1378}
1379
1380#[derive(Subcommand, Debug)]
1381pub enum MonitorSubCommand {
1382 Check,
1383 Start,
1384}
1385
1386#[cfg(feature = "monitoring")]
1387#[derive(Args, Debug)]
1388pub struct MonitoringCmd {
1389 #[command(subcommand)]
1390 pub command: MonitoringSubCommand,
1391}
1392
1393#[cfg(feature = "monitoring")]
1394#[derive(Subcommand, Debug)]
1395pub enum MonitoringSubCommand {
1396 Serve {
1397 #[arg(
1398 short,
1399 long,
1400 default_value = "prodzilla.yml",
1401 help = "Monitoring config file"
1402 )]
1403 file: String,
1404 },
1405 RunOnce {
1406 #[arg(
1407 short,
1408 long,
1409 default_value = "prodzilla.yml",
1410 help = "Monitoring config file"
1411 )]
1412 file: String,
1413 #[arg(long, help = "Run probes only")]
1414 probes_only: bool,
1415 #[arg(long, help = "Run stories only")]
1416 stories_only: bool,
1417 },
1418 List {
1419 #[arg(
1420 short,
1421 long,
1422 default_value = "prodzilla.yml",
1423 help = "Monitoring config file"
1424 )]
1425 file: String,
1426 },
1427}
1428
1429#[derive(Args, Debug)]
1430#[command(
1431 arg_required_else_help = true,
1432 disable_help_subcommand = true,
1433 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1434 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."
1435)]
1436pub struct ApiCmd {
1437 #[command(subcommand)]
1438 pub command: ApiSubCommand,
1439}
1440
1441#[derive(Args, Debug)]
1442#[command(
1443 arg_required_else_help = true,
1444 disable_help_subcommand = true,
1445 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1446 after_help = "Examples:\n xbp runners orgs list\n xbp runners groups list --organization-id <uuid>\n xbp runners groups create --organization-id <uuid> --name builders --visibility selected\n xbp runners groups access <runner-group-id>\n xbp runners inventory --organization-id <uuid>\n xbp runners hosts enroll --organization-id <uuid> --platform linux --hostname runner-1 --runner-name-prefix xbp-linux\n xbp runners hosts preflight --runner-host-id <uuid>\n xbp runners status --organization-id <uuid>\n xbp runners deploy --organization-id <uuid> --runner-host-id <uuid>\n xbp runners agent serve --runner-host-id <uuid>"
1447)]
1448pub struct RunnersCmd {
1449 #[command(subcommand)]
1450 pub command: RunnersSubCommand,
1451}
1452
1453#[derive(Subcommand, Debug)]
1454pub enum RunnersSubCommand {
1455 #[command(about = "List organizations already registered in the XBP control plane")]
1456 OrgsList(RunnersOrgsListCmd),
1457 #[command(about = "Manage runner groups")]
1458 Groups(RunnersGroupsCmd),
1459 #[command(about = "List or enroll runner hosts")]
1460 Hosts(RunnersHostsCmd),
1461 #[command(about = "List runner inventory for an organization")]
1462 Inventory(RunnersInventoryCmd),
1463 #[command(about = "Show runner status for an organization or host")]
1464 Status(RunnersStatusCmd),
1465 #[command(about = "Show recent runner job logs and failures")]
1466 Logs(RunnersLogsCmd),
1467 #[command(about = "Queue a runner sync job")]
1468 Sync(RunnersSyncCmd),
1469 #[command(about = "Queue a runner deployment job")]
1470 Deploy(RunnersDeployCmd),
1471 #[command(about = "Queue a runner update job")]
1472 Update(RunnersUpdateCmd),
1473 #[command(about = "Queue a runner removal job")]
1474 Remove(RunnersRemoveCmd),
1475 #[command(about = "Run a local runner host poller that claims and executes jobs")]
1476 Agent(RunnersAgentCmd),
1477}
1478
1479#[derive(Args, Debug)]
1480pub struct RunnersOrgsListCmd {
1481 #[command(flatten)]
1482 pub target: ApiTargetOptions,
1483}
1484
1485#[derive(Args, Debug)]
1486pub struct RunnersGroupsCmd {
1487 #[command(subcommand)]
1488 pub command: RunnersGroupsSubCommand,
1489}
1490
1491#[derive(Subcommand, Debug)]
1492pub enum RunnersGroupsSubCommand {
1493 #[command(about = "List runner groups for an organization")]
1494 List(RunnersGroupsListCmd),
1495 #[command(about = "Create or upsert a runner group")]
1496 Create(RunnersGroupsCreateCmd),
1497 #[command(about = "Update an existing runner group")]
1498 Update(RunnersGroupsCreateCmd),
1499 #[command(about = "Delete a runner group")]
1500 Delete(RunnersGroupsDeleteCmd),
1501 #[command(about = "Inspect effective repository access for a runner group")]
1502 Access(RunnersGroupsAccessCmd),
1503}
1504
1505#[derive(Args, Debug)]
1506pub struct RunnersGroupsListCmd {
1507 #[arg(long, help = "Organization ID")]
1508 pub organization_id: String,
1509 #[command(flatten)]
1510 pub target: ApiTargetOptions,
1511}
1512
1513#[derive(Args, Debug, Clone)]
1514pub struct RunnersGroupsCreateCmd {
1515 #[arg(long, help = "Organization ID")]
1516 pub organization_id: String,
1517 #[arg(long, help = "Runner group name")]
1518 pub name: String,
1519 #[arg(long, help = "Visibility: all, selected, or private")]
1520 pub visibility: Option<String>,
1521 #[arg(long, help = "Optional GitHub installation ID")]
1522 pub github_installation_id: Option<String>,
1523 #[arg(long, help = "Optional GitHub runner group ID")]
1524 pub github_runner_group_id: Option<i64>,
1525 #[arg(long, help = "Optional sync state")]
1526 pub sync_state: Option<String>,
1527 #[arg(long, help = "Mark group as inherited")]
1528 pub inherited: bool,
1529 #[arg(long, help = "Allow public repositories")]
1530 pub allows_public_repositories: bool,
1531 #[arg(long, help = "Prompt for visibility and repository access settings")]
1532 pub interactive: bool,
1533 #[arg(long, help = "Restrict group to workflows")]
1534 pub restricted_to_workflows: bool,
1535 #[arg(long, help = "Selected workflows JSON array")]
1536 pub selected_workflows_json: Option<String>,
1537 #[arg(long, help = "Metadata JSON object")]
1538 pub metadata_json: Option<String>,
1539 #[command(flatten)]
1540 pub target: ApiTargetOptions,
1541}
1542
1543#[derive(Args, Debug)]
1544pub struct RunnersGroupsDeleteCmd {
1545 #[arg(help = "Runner group ID")]
1546 pub runner_group_id: String,
1547 #[command(flatten)]
1548 pub target: ApiTargetOptions,
1549}
1550
1551#[derive(Args, Debug)]
1552pub struct RunnersGroupsAccessCmd {
1553 #[arg(help = "Runner group ID")]
1554 pub runner_group_id: String,
1555 #[command(flatten)]
1556 pub target: ApiTargetOptions,
1557}
1558
1559#[derive(Args, Debug)]
1560pub struct RunnersHostsCmd {
1561 #[command(subcommand)]
1562 pub command: RunnersHostsSubCommand,
1563}
1564
1565#[derive(Subcommand, Debug)]
1566pub enum RunnersHostsSubCommand {
1567 #[command(about = "List runner hosts")]
1568 List(RunnersHostsListCmd),
1569 #[command(about = "Enroll or upsert a runner host")]
1570 Enroll(Box<RunnersHostsEnrollCmd>),
1571 #[command(about = "Run host preflight checks and persist the result")]
1572 Preflight(RunnersHostsPreflightCmd),
1573 #[command(about = "Inspect host enrollment, heartbeat, and preflight state")]
1574 Status(RunnersHostsStatusCmd),
1575}
1576
1577#[derive(Args, Debug)]
1578pub struct RunnersHostsListCmd {
1579 #[arg(long, help = "Optional organization ID")]
1580 pub organization_id: Option<String>,
1581 #[arg(long, help = "Optional daemon ID")]
1582 pub daemon_id: Option<String>,
1583 #[arg(long, help = "Optional status filter")]
1584 pub status: Option<String>,
1585 #[command(flatten)]
1586 pub target: ApiTargetOptions,
1587}
1588
1589#[derive(Args, Debug)]
1590pub struct RunnersHostsEnrollCmd {
1591 #[arg(long, help = "Organization ID")]
1592 pub organization_id: String,
1593 #[arg(long, help = "Optional daemon ID for daemon-backed hosts")]
1594 pub daemon_id: Option<String>,
1595 #[arg(long, help = "Host kind: daemon or rogue")]
1596 pub host_kind: String,
1597 #[arg(long, help = "Platform: linux, windows, or macos")]
1598 pub platform: String,
1599 #[arg(long, help = "Hostname")]
1600 pub hostname: String,
1601 #[arg(long, help = "Runner name prefix")]
1602 pub runner_name_prefix: String,
1603 #[arg(long, help = "Optional display name")]
1604 pub display_name: Option<String>,
1605 #[arg(long, help = "Optional architecture")]
1606 pub arch: Option<String>,
1607 #[arg(long, help = "Optional status")]
1608 pub status: Option<String>,
1609 #[arg(long, help = "Labels JSON object")]
1610 pub labels_json: Option<String>,
1611 #[arg(long, help = "Capabilities JSON object")]
1612 pub capabilities_json: Option<String>,
1613 #[arg(long, help = "Metadata JSON object")]
1614 pub metadata_json: Option<String>,
1615 #[command(flatten)]
1616 pub target: ApiTargetOptions,
1617}
1618
1619#[derive(Args, Debug)]
1620pub struct RunnersHostsPreflightCmd {
1621 #[arg(long, help = "Runner host ID")]
1622 pub runner_host_id: String,
1623 #[arg(
1624 long,
1625 help = "Persist the preflight result back to the control-plane API"
1626 )]
1627 pub write_api: bool,
1628 #[command(flatten)]
1629 pub target: ApiTargetOptions,
1630}
1631
1632#[derive(Args, Debug)]
1633pub struct RunnersHostsStatusCmd {
1634 #[arg(long, help = "Runner host ID")]
1635 pub runner_host_id: String,
1636 #[command(flatten)]
1637 pub target: ApiTargetOptions,
1638}
1639
1640#[derive(Args, Debug)]
1641pub struct RunnersInventoryCmd {
1642 #[arg(long, help = "Organization ID")]
1643 pub organization_id: String,
1644 #[command(flatten)]
1645 pub target: ApiTargetOptions,
1646}
1647
1648#[derive(Args, Debug)]
1649pub struct RunnersStatusCmd {
1650 #[arg(long, help = "Organization ID")]
1651 pub organization_id: String,
1652 #[arg(long, help = "Optional runner host ID")]
1653 pub runner_host_id: Option<String>,
1654 #[arg(long, help = "Optional runner ID")]
1655 pub runner_id: Option<String>,
1656 #[arg(long, help = "Optional daemon ID")]
1657 pub daemon_id: Option<String>,
1658 #[arg(long, help = "Optional status filter")]
1659 pub status: Option<String>,
1660 #[arg(long, help = "Optional phase filter")]
1661 pub phase: Option<String>,
1662 #[arg(long, default_value_t = 20, help = "Maximum jobs to show")]
1663 pub limit: usize,
1664 #[command(flatten)]
1665 pub target: ApiTargetOptions,
1666}
1667
1668#[derive(Args, Debug)]
1669pub struct RunnersLogsCmd {
1670 #[arg(long, help = "Optional organization ID")]
1671 pub organization_id: Option<String>,
1672 #[arg(long, help = "Optional runner host ID")]
1673 pub runner_host_id: Option<String>,
1674 #[arg(long, help = "Optional runner ID")]
1675 pub runner_id: Option<String>,
1676 #[arg(long, help = "Optional daemon ID")]
1677 pub daemon_id: Option<String>,
1678 #[arg(long, help = "Optional status filter")]
1679 pub status: Option<String>,
1680 #[arg(long, help = "Optional phase filter")]
1681 pub phase: Option<String>,
1682 #[arg(long, default_value_t = 10, help = "Maximum jobs to show")]
1683 pub limit: usize,
1684 #[command(flatten)]
1685 pub target: ApiTargetOptions,
1686}
1687
1688#[derive(Args, Debug)]
1689pub struct RunnersSyncCmd {
1690 #[arg(long, help = "Organization ID")]
1691 pub organization_id: String,
1692 #[arg(long, help = "Optional runner host ID")]
1693 pub runner_host_id: Option<String>,
1694 #[arg(long, help = "Optional daemon ID")]
1695 pub daemon_id: Option<String>,
1696 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1697 pub run_after: Option<String>,
1698 #[arg(long, help = "Payload JSON object")]
1699 pub payload_json: Option<String>,
1700 #[command(flatten)]
1701 pub target: ApiTargetOptions,
1702}
1703
1704#[derive(Args, Debug)]
1705pub struct RunnersDeployCmd {
1706 #[arg(long, help = "Organization ID")]
1707 pub organization_id: String,
1708 #[arg(long, help = "Optional runner host ID")]
1709 pub runner_host_id: Option<String>,
1710 #[arg(long, help = "Optional daemon ID")]
1711 pub daemon_id: Option<String>,
1712 #[arg(long, help = "Optional existing runner ID")]
1713 pub runner_id: Option<String>,
1714 #[arg(long, help = "Optional priority")]
1715 pub priority: Option<i32>,
1716 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1717 pub run_after: Option<String>,
1718 #[arg(long, help = "Payload JSON object")]
1719 pub payload_json: Option<String>,
1720 #[command(flatten)]
1721 pub target: ApiTargetOptions,
1722}
1723
1724#[derive(Args, Debug)]
1725pub struct RunnersUpdateCmd {
1726 #[arg(long, help = "Organization ID")]
1727 pub organization_id: String,
1728 #[arg(long, help = "Optional runner host ID")]
1729 pub runner_host_id: Option<String>,
1730 #[arg(long, help = "Optional daemon ID")]
1731 pub daemon_id: Option<String>,
1732 #[arg(long, help = "Optional existing runner ID")]
1733 pub runner_id: Option<String>,
1734 #[arg(long, help = "Optional priority")]
1735 pub priority: Option<i32>,
1736 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1737 pub run_after: Option<String>,
1738 #[arg(long, help = "Payload JSON object")]
1739 pub payload_json: Option<String>,
1740 #[command(flatten)]
1741 pub target: ApiTargetOptions,
1742}
1743
1744#[derive(Args, Debug)]
1745pub struct RunnersRemoveCmd {
1746 #[arg(long, help = "Organization ID")]
1747 pub organization_id: String,
1748 #[arg(long, help = "Existing runner ID")]
1749 pub runner_id: String,
1750 #[arg(long, help = "Optional runner host ID")]
1751 pub runner_host_id: Option<String>,
1752 #[arg(long, help = "Optional daemon ID")]
1753 pub daemon_id: Option<String>,
1754 #[arg(long, help = "Optional priority")]
1755 pub priority: Option<i32>,
1756 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
1757 pub run_after: Option<String>,
1758 #[arg(long, help = "Payload JSON object")]
1759 pub payload_json: Option<String>,
1760 #[command(flatten)]
1761 pub target: ApiTargetOptions,
1762}
1763
1764#[derive(Args, Debug)]
1765pub struct RunnersAgentCmd {
1766 #[command(subcommand)]
1767 pub command: RunnersAgentSubCommand,
1768}
1769
1770#[derive(Subcommand, Debug)]
1771pub enum RunnersAgentSubCommand {
1772 #[command(about = "Serve as a local runner host agent")]
1773 Serve(RunnersAgentServeCmd),
1774}
1775
1776#[derive(Args, Debug)]
1777pub struct RunnersAgentServeCmd {
1778 #[arg(long, help = "Runner host ID")]
1779 pub runner_host_id: String,
1780 #[arg(long, default_value_t = 15, help = "Polling interval in seconds")]
1781 pub interval_seconds: u64,
1782 #[arg(long, help = "Process at most one job and exit")]
1783 pub once: bool,
1784 #[command(flatten)]
1785 pub target: ApiTargetOptions,
1786}
1787
1788#[derive(Args, Debug)]
1789#[command(
1790 arg_required_else_help = true,
1791 disable_help_subcommand = true,
1792 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1793 after_help = "Examples:\n xbp mcp serve\n xbp mcp serve --tray\n xbp mcp serve --detach\n xbp mcp config\n xbp mcp inspector --json\n xbp mcp status\n xbp mcp install --port 1113\n\nCursor MCP url: http://127.0.0.1:1113/sse"
1794)]
1795pub struct McpCmd {
1796 #[command(subcommand)]
1797 pub command: McpSubCommand,
1798}
1799
1800#[derive(Subcommand, Debug)]
1801pub enum McpSubCommand {
1802 #[command(about = "Start the MCP HTTP/SSE server (foreground or detached)")]
1803 Serve(McpServeCmd),
1804 #[command(about = "Install and enable xbp-mcp.service via systemd")]
1805 Install(McpInstallCmd),
1806 #[command(about = "Check whether the MCP server is reachable")]
1807 Status(McpStatusCmd),
1808 #[command(about = "Print Cursor MCP configuration JSON")]
1809 Config(McpConfigCmd),
1810 #[command(about = "Print prefilled MCP Inspector URLs and tool-call presets")]
1811 Inspector(McpInspectorCmd),
1812}
1813
1814#[derive(Args, Debug)]
1815pub struct McpServeCmd {
1816 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1817 pub bind: String,
1818 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1819 pub port: u16,
1820 #[arg(
1821 long,
1822 help = "Spawn a background MCP server process and print Cursor config"
1823 )]
1824 pub detach: bool,
1825 #[arg(long, help = "Use stdio MCP transport instead of HTTP/SSE")]
1826 pub stdio: bool,
1827 #[arg(
1828 long,
1829 help = "Show a system tray icon (xbp.app icon) with a Quit action"
1830 )]
1831 pub tray: bool,
1832}
1833
1834#[derive(Args, Debug)]
1835pub struct McpInstallCmd {
1836 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1837 pub bind: String,
1838 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1839 pub port: u16,
1840}
1841
1842#[derive(Args, Debug)]
1843pub struct McpStatusCmd {
1844 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1845 pub bind: String,
1846 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1847 pub port: u16,
1848}
1849
1850#[derive(Args, Debug)]
1851pub struct McpConfigCmd {
1852 #[arg(long, default_value = "127.0.0.1", help = "Bind address")]
1853 pub bind: String,
1854 #[arg(long, default_value_t = 1113, help = "HTTP port for MCP SSE transport")]
1855 pub port: u16,
1856}
1857
1858#[derive(Args, Debug)]
1859pub struct McpInspectorCmd {
1860 #[arg(
1861 long,
1862 default_value = "127.0.0.1",
1863 help = "Bind address for the xbp MCP server"
1864 )]
1865 pub bind: String,
1866 #[arg(
1867 long,
1868 default_value_t = 1113,
1869 help = "HTTP port for the xbp MCP SSE transport"
1870 )]
1871 pub port: u16,
1872 #[arg(long, default_value_t = 6274, help = "MCP Inspector UI port")]
1873 pub inspector_port: u16,
1874 #[arg(
1875 long,
1876 help = "Write .xbp/mcp-inspector.json for `npx @modelcontextprotocol/inspector --config`"
1877 )]
1878 pub write_config: bool,
1879 #[arg(
1880 long,
1881 help = "Launch MCP Inspector via npx (writes config first; requires Node.js)"
1882 )]
1883 pub launch: bool,
1884 #[arg(long, help = "Emit machine-readable JSON playbook")]
1885 pub json: bool,
1886}
1887
1888#[derive(Args, Debug)]
1889#[command(
1890 arg_required_else_help = true,
1891 disable_help_subcommand = true,
1892 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
1893 after_help = "Examples:\n xbp worktree-watch start\n xbp worktree-watch start --detach\n xbp worktree-watch start --parent C:\\Users\\floris\\Documents\\GitHub --detach\n xbp worktree-watch stop\n xbp worktree-watch sync\n xbp worktree-watch status"
1894)]
1895pub struct WorktreeWatchCmd {
1896 #[command(subcommand)]
1897 pub command: WorktreeWatchSubCommand,
1898}
1899
1900#[derive(Subcommand, Debug)]
1901pub enum WorktreeWatchSubCommand {
1902 #[command(about = "Watch the current git worktree and append mutation events locally")]
1903 Start(WorktreeWatchStartCmd),
1904 #[command(about = "Stop a detached background worktree watcher")]
1905 Stop(WorktreeWatchStopCmd),
1906 #[command(about = "Upload unsynced local mutation spool files to xbp.app")]
1907 Sync(WorktreeWatchSyncCmd),
1908 #[command(about = "Show the resolved spool path and local sync backlog")]
1909 Status(WorktreeWatchStatusCmd),
1910}
1911
1912#[derive(Args, Debug, Clone)]
1913pub struct WorktreeWatchTarget {
1914 #[arg(
1915 long,
1916 conflicts_with = "parent",
1917 help = "Git repository root to watch; defaults to the current repo"
1918 )]
1919 pub repo: Option<PathBuf>,
1920 #[arg(
1921 long,
1922 conflicts_with = "repo",
1923 help = "Folder containing git repositories to target recursively"
1924 )]
1925 pub parent: Option<PathBuf>,
1926}
1927
1928#[derive(Args, Debug)]
1929pub struct WorktreeWatchStartCmd {
1930 #[command(flatten)]
1931 pub target: WorktreeWatchTarget,
1932 #[arg(long, help = "Spawn the watcher as a detached background process")]
1933 pub detach: bool,
1934 #[arg(
1935 long,
1936 default_value_t = 60,
1937 help = "Upload queued events every N seconds while watching; set 0 to disable periodic sync"
1938 )]
1939 pub sync_interval_seconds: u64,
1940 #[arg(long, help = "Record current git commit state once and exit")]
1941 pub once: bool,
1942}
1943
1944#[derive(Args, Debug)]
1945pub struct WorktreeWatchStopCmd {
1946 #[command(flatten)]
1947 pub target: WorktreeWatchTarget,
1948 #[arg(
1949 long,
1950 help = "Stop the PID from watcher state even when the command line cannot be verified"
1951 )]
1952 pub force: bool,
1953}
1954
1955#[derive(Args, Debug)]
1956pub struct WorktreeWatchSyncCmd {
1957 #[command(flatten)]
1958 pub target: WorktreeWatchTarget,
1959 #[arg(long, help = "Print the upload plan without sending it to xbp.app")]
1960 pub dry_run: bool,
1961 #[arg(
1962 long,
1963 help = "Upload all local JSONL spool files, including files already marked synced"
1964 )]
1965 pub resync: bool,
1966}
1967
1968#[derive(Args, Debug)]
1969pub struct WorktreeWatchStatusCmd {
1970 #[command(flatten)]
1971 pub target: WorktreeWatchTarget,
1972 #[arg(long, help = "Emit status as JSON")]
1973 pub json: bool,
1974 #[arg(
1975 long,
1976 help = "Print decoded mutation and commit records from local JSONL spool files"
1977 )]
1978 pub records: bool,
1979 #[arg(
1980 long,
1981 default_value_t = 50,
1982 requires = "records",
1983 help = "Maximum decoded JSONL records to print per repository"
1984 )]
1985 pub record_limit: usize,
1986 #[arg(
1987 long,
1988 help = "Generate and print persisted activity statistics from local JSONL spool files"
1989 )]
1990 pub stats: bool,
1991 #[arg(
1992 long,
1993 default_value_t = 15,
1994 requires = "stats",
1995 help = "Maximum minutes between mutation events counted as one coding session"
1996 )]
1997 pub stats_gap_minutes: u64,
1998}
1999
2000#[derive(Args, Debug, Clone, Default)]
2001pub struct ApiTargetOptions {
2002 #[arg(long, help = "Override the request base URL for this command")]
2003 pub base_url: Option<String>,
2004 #[arg(
2005 long,
2006 help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
2007 )]
2008 pub web: bool,
2009 #[arg(
2010 long,
2011 help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
2012 )]
2013 pub no_auth: bool,
2014 #[arg(
2015 long,
2016 help = "Extra header in 'Name: Value' format",
2017 value_name = "HEADER"
2018 )]
2019 pub header: Vec<String>,
2020 #[arg(long, help = "Print response headers")]
2021 pub include_headers: bool,
2022 #[arg(
2023 long,
2024 help = "Print the response body as-is without JSON pretty formatting"
2025 )]
2026 pub raw: bool,
2027}
2028
2029#[cfg(feature = "docker")]
2030#[derive(Args, Debug)]
2031pub struct DockerCmd {
2032 #[arg(
2033 trailing_var_arg = true,
2034 allow_hyphen_values = true,
2035 help = "Arguments to pass directly to the Docker CLI (default: --help)"
2036 )]
2037 pub args: Vec<String>,
2038}
2039
2040#[derive(Subcommand, Debug)]
2041pub enum ApiSubCommand {
2042 #[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
2043 Install {
2044 #[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
2045 port: u16,
2046 },
2047 #[command(about = "Call the XBP API health endpoint")]
2048 Health(ApiHealthCmd),
2049 #[command(about = "Manage XBP control-plane projects")]
2050 Projects(ApiProjectsCmd),
2051 #[command(about = "Manage XBP daemon registrations and heartbeats")]
2052 Daemons(ApiDaemonsCmd),
2053 #[command(about = "Manage XBP deployment jobs")]
2054 Jobs(ApiJobsCmd),
2055 #[command(about = "Manage XBP runner hosts, groups, inventory, and runner jobs")]
2056 Runners(ApiRunnersCmd),
2057 #[command(about = "Manage XBP proxy routes on the local API server")]
2058 Routes(ApiRoutesCmd),
2059 #[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
2060 Request(ApiRequestCmd),
2061}
2062
2063#[derive(Args, Debug)]
2064pub struct ApiHealthCmd {
2065 #[command(flatten)]
2066 pub target: ApiTargetOptions,
2067}
2068
2069#[derive(Args, Debug)]
2070pub struct ApiProjectsCmd {
2071 #[command(subcommand)]
2072 pub command: ApiProjectsSubCommand,
2073}
2074
2075#[derive(Subcommand, Debug)]
2076pub enum ApiProjectsSubCommand {
2077 #[command(about = "List projects from the XBP control-plane API")]
2078 List(ApiProjectsListCmd),
2079 #[command(about = "Create or upsert a control-plane project")]
2080 Create(Box<ApiProjectsCreateCmd>),
2081}
2082
2083#[derive(Args, Debug)]
2084pub struct ApiProjectsListCmd {
2085 #[arg(long, help = "Optional organization ID filter")]
2086 pub organization_id: Option<String>,
2087 #[command(flatten)]
2088 pub target: ApiTargetOptions,
2089}
2090
2091#[derive(Args, Debug)]
2092pub struct ApiProjectsCreateCmd {
2093 #[arg(long, help = "Project name")]
2094 pub name: String,
2095 #[arg(long, help = "Project path or repo path key")]
2096 pub path: String,
2097 #[arg(long, help = "Optional organization ID")]
2098 pub organization_id: Option<String>,
2099 #[arg(long, help = "Optional project slug")]
2100 pub slug: Option<String>,
2101 #[arg(long, help = "Optional project version")]
2102 pub version: Option<String>,
2103 #[arg(long, help = "Optional build directory")]
2104 pub build_dir: Option<String>,
2105 #[arg(long, help = "Optional runtime enum value")]
2106 pub runtime: Option<String>,
2107 #[arg(long, help = "Optional default branch")]
2108 pub default_branch: Option<String>,
2109 #[arg(long, help = "Optional repository root directory")]
2110 pub root_directory: Option<String>,
2111 #[arg(long, help = "Optional build command")]
2112 pub build_command: Option<String>,
2113 #[arg(long, help = "Optional install command")]
2114 pub install_command: Option<String>,
2115 #[arg(long, help = "Optional start command")]
2116 pub start_command: Option<String>,
2117 #[arg(long, help = "Optional output directory")]
2118 pub output_directory: Option<String>,
2119 #[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
2120 pub repository_json: Option<String>,
2121 #[arg(long, help = "Runtime policy JSON payload")]
2122 pub runtime_policy_json: Option<String>,
2123 #[arg(long, help = "Metadata JSON object")]
2124 pub metadata_json: Option<String>,
2125 #[command(flatten)]
2126 pub target: ApiTargetOptions,
2127}
2128
2129#[derive(Args, Debug)]
2130pub struct ApiDaemonsCmd {
2131 #[command(subcommand)]
2132 pub command: ApiDaemonsSubCommand,
2133}
2134
2135#[derive(Subcommand, Debug)]
2136pub enum ApiDaemonsSubCommand {
2137 #[command(about = "List registered daemons")]
2138 List(ApiDaemonsListCmd),
2139 #[command(about = "Register or upsert a daemon record")]
2140 Register(ApiDaemonsRegisterCmd),
2141 #[command(about = "Post a heartbeat update for a daemon")]
2142 Heartbeat(ApiDaemonsHeartbeatCmd),
2143 #[command(about = "Update daemon status only")]
2144 UpdateStatus(ApiDaemonsUpdateStatusCmd),
2145}
2146
2147#[derive(Args, Debug)]
2148pub struct ApiDaemonsListCmd {
2149 #[command(flatten)]
2150 pub target: ApiTargetOptions,
2151}
2152
2153#[derive(Args, Debug)]
2154pub struct ApiDaemonsRegisterCmd {
2155 #[arg(long, help = "Daemon node name")]
2156 pub node_name: String,
2157 #[arg(long, help = "Daemon hostname")]
2158 pub hostname: String,
2159 #[arg(long, help = "Daemon binary version")]
2160 pub version: String,
2161 #[arg(long, help = "Optional region")]
2162 pub region: Option<String>,
2163 #[arg(long, help = "Optional public IP")]
2164 pub public_ip: Option<String>,
2165 #[arg(long, help = "Optional internal IP")]
2166 pub internal_ip: Option<String>,
2167 #[arg(long, help = "Optional status enum value")]
2168 pub status: Option<String>,
2169 #[arg(long, help = "Optional CPU core count")]
2170 pub cpu_cores: Option<i32>,
2171 #[arg(long, help = "Optional total memory in MB")]
2172 pub memory_total_mb: Option<i32>,
2173 #[arg(long, help = "Optional total disk in GB")]
2174 pub disk_total_gb: Option<i32>,
2175 #[arg(long, help = "Labels JSON object")]
2176 pub labels_json: Option<String>,
2177 #[arg(long, help = "Metadata JSON object")]
2178 pub metadata_json: Option<String>,
2179 #[command(flatten)]
2180 pub target: ApiTargetOptions,
2181}
2182
2183#[derive(Args, Debug)]
2184pub struct ApiDaemonsHeartbeatCmd {
2185 #[arg(help = "Daemon ID")]
2186 pub daemon_id: String,
2187 #[arg(long, help = "Optional status enum value")]
2188 pub status: Option<String>,
2189 #[arg(long, help = "Optional daemon version")]
2190 pub version: Option<String>,
2191 #[arg(long, help = "Optional public IP")]
2192 pub public_ip: Option<String>,
2193 #[arg(long, help = "Optional internal IP")]
2194 pub internal_ip: Option<String>,
2195 #[arg(long, help = "Optional CPU core count")]
2196 pub cpu_cores: Option<i32>,
2197 #[arg(long, help = "Optional total memory in MB")]
2198 pub memory_total_mb: Option<i32>,
2199 #[arg(long, help = "Optional total disk in GB")]
2200 pub disk_total_gb: Option<i32>,
2201 #[arg(long, help = "Labels JSON object")]
2202 pub labels_json: Option<String>,
2203 #[command(flatten)]
2204 pub target: ApiTargetOptions,
2205}
2206
2207#[derive(Args, Debug)]
2208pub struct ApiDaemonsUpdateStatusCmd {
2209 #[arg(help = "Daemon ID")]
2210 pub daemon_id: String,
2211 #[arg(long, help = "Daemon status enum value")]
2212 pub status: String,
2213 #[command(flatten)]
2214 pub target: ApiTargetOptions,
2215}
2216
2217#[derive(Args, Debug)]
2218pub struct ApiJobsCmd {
2219 #[command(subcommand)]
2220 pub command: ApiJobsSubCommand,
2221}
2222
2223#[derive(Args, Debug)]
2224pub struct ApiRunnersCmd {
2225 #[command(subcommand)]
2226 pub command: ApiRunnersSubCommand,
2227}
2228
2229#[derive(Subcommand, Debug)]
2230pub enum ApiRunnersSubCommand {
2231 #[command(about = "List runner hosts")]
2232 HostsList(ApiRunnersHostsListCmd),
2233 #[command(about = "Register or upsert a runner host")]
2234 HostsEnroll(ApiRunnersHostsEnrollCmd),
2235 #[command(about = "Post a heartbeat update for a runner host")]
2236 HostsHeartbeat(ApiRunnersHostsHeartbeatCmd),
2237 #[command(about = "List persisted preflight records for a runner host")]
2238 HostsPreflightsList(ApiRunnersHostsPreflightsListCmd),
2239 #[command(about = "Create or replace a persisted preflight record for a runner host")]
2240 HostsPreflightsUpsert(ApiRunnersHostsPreflightsUpsertCmd),
2241 #[command(about = "List runner groups for an organization")]
2242 GroupsList(ApiRunnersGroupsListCmd),
2243 #[command(about = "Create or upsert a runner group")]
2244 GroupsCreate(ApiRunnersGroupsCreateCmd),
2245 #[command(about = "Update an existing runner group")]
2246 GroupsUpdate(ApiRunnersGroupsCreateCmd),
2247 #[command(about = "Delete a runner group")]
2248 GroupsDelete(ApiRunnersGroupsDeleteCmd),
2249 #[command(about = "List repository access for a runner group")]
2250 GroupRepositoriesList(ApiRunnersGroupRepositoriesListCmd),
2251 #[command(about = "Grant or upsert repository access for a runner group")]
2252 GroupRepositoriesCreate(ApiRunnersGroupRepositoriesCreateCmd),
2253 #[command(about = "List runner inventory for an organization")]
2254 InventoryList(ApiRunnersInventoryListCmd),
2255 #[command(about = "Upsert a runner inventory record")]
2256 InventoryUpsert(ApiRunnersInventoryUpsertCmd),
2257 #[command(about = "List runner jobs")]
2258 JobsList(ApiRunnersJobsListCmd),
2259 #[command(about = "Create a runner job")]
2260 JobsCreate(ApiRunnersJobsCreateCmd),
2261 #[command(about = "Claim the next runner job")]
2262 JobsClaim(ApiRunnersJobsClaimCmd),
2263 #[command(about = "Update a runner job")]
2264 JobsUpdate(ApiRunnersJobsUpdateCmd),
2265}
2266
2267#[derive(Args, Debug)]
2268pub struct ApiRunnersHostsListCmd {
2269 #[arg(long)]
2270 pub organization_id: Option<String>,
2271 #[arg(long)]
2272 pub daemon_id: Option<String>,
2273 #[arg(long)]
2274 pub status: Option<String>,
2275 #[command(flatten)]
2276 pub target: ApiTargetOptions,
2277}
2278
2279#[derive(Args, Debug)]
2280pub struct ApiRunnersHostsEnrollCmd {
2281 #[arg(long)]
2282 pub organization_id: String,
2283 #[arg(long)]
2284 pub daemon_id: Option<String>,
2285 #[arg(long)]
2286 pub host_kind: String,
2287 #[arg(long)]
2288 pub platform: String,
2289 #[arg(long)]
2290 pub hostname: String,
2291 #[arg(long)]
2292 pub runner_name_prefix: String,
2293 #[arg(long)]
2294 pub display_name: Option<String>,
2295 #[arg(long)]
2296 pub arch: Option<String>,
2297 #[arg(long)]
2298 pub status: Option<String>,
2299 #[arg(long)]
2300 pub labels_json: Option<String>,
2301 #[arg(long)]
2302 pub capabilities_json: Option<String>,
2303 #[arg(long)]
2304 pub metadata_json: Option<String>,
2305 #[command(flatten)]
2306 pub target: ApiTargetOptions,
2307}
2308
2309#[derive(Args, Debug)]
2310pub struct ApiRunnersHostsHeartbeatCmd {
2311 #[arg(help = "Runner host ID")]
2312 pub runner_host_id: String,
2313 #[arg(long)]
2314 pub status: Option<String>,
2315 #[arg(long)]
2316 pub labels_json: Option<String>,
2317 #[arg(long)]
2318 pub capabilities_json: Option<String>,
2319 #[command(flatten)]
2320 pub target: ApiTargetOptions,
2321}
2322
2323#[derive(Args, Debug)]
2324pub struct ApiRunnersHostsPreflightsListCmd {
2325 #[arg(long)]
2326 pub runner_host_id: String,
2327 #[command(flatten)]
2328 pub target: ApiTargetOptions,
2329}
2330
2331#[derive(Args, Debug)]
2332pub struct ApiRunnersHostsPreflightsUpsertCmd {
2333 #[arg(long)]
2334 pub runner_host_id: String,
2335 #[arg(long)]
2336 pub platform: String,
2337 #[arg(long)]
2338 pub status: String,
2339 #[arg(long)]
2340 pub docker_available: Option<bool>,
2341 #[arg(long)]
2342 pub service_manager: Option<String>,
2343 #[arg(long)]
2344 pub checks_json: Option<String>,
2345 #[arg(long)]
2346 pub checked_at: Option<String>,
2347 #[command(flatten)]
2348 pub target: ApiTargetOptions,
2349}
2350
2351#[derive(Args, Debug, Clone)]
2352pub struct ApiRunnersGroupsListCmd {
2353 #[arg(long)]
2354 pub organization_id: String,
2355 #[command(flatten)]
2356 pub target: ApiTargetOptions,
2357}
2358
2359#[derive(Args, Debug, Clone)]
2360pub struct ApiRunnersGroupsCreateCmd {
2361 #[arg(long)]
2362 pub organization_id: String,
2363 #[arg(long)]
2364 pub name: String,
2365 #[arg(long)]
2366 pub visibility: Option<String>,
2367 #[arg(long)]
2368 pub github_installation_id: Option<String>,
2369 #[arg(long)]
2370 pub github_runner_group_id: Option<i64>,
2371 #[arg(long)]
2372 pub sync_state: Option<String>,
2373 #[arg(long)]
2374 pub sync_error: Option<String>,
2375 #[arg(long)]
2376 pub inherited: bool,
2377 #[arg(long)]
2378 pub allows_public_repositories: bool,
2379 #[arg(long)]
2380 pub interactive: bool,
2381 #[arg(long)]
2382 pub restricted_to_workflows: bool,
2383 #[arg(long)]
2384 pub selected_workflows_json: Option<String>,
2385 #[arg(long)]
2386 pub metadata_json: Option<String>,
2387 #[command(flatten)]
2388 pub target: ApiTargetOptions,
2389}
2390
2391#[derive(Args, Debug)]
2392pub struct ApiRunnersGroupsDeleteCmd {
2393 #[arg(help = "Runner group ID")]
2394 pub runner_group_id: String,
2395 #[command(flatten)]
2396 pub target: ApiTargetOptions,
2397}
2398
2399#[derive(Args, Debug)]
2400pub struct ApiRunnersGroupRepositoriesListCmd {
2401 #[arg(help = "Runner group ID")]
2402 pub runner_group_id: String,
2403 #[command(flatten)]
2404 pub target: ApiTargetOptions,
2405}
2406
2407#[derive(Args, Debug)]
2408pub struct ApiRunnersGroupRepositoriesCreateCmd {
2409 #[arg(help = "Runner group ID")]
2410 pub runner_group_id: String,
2411 #[arg(long)]
2412 pub github_repository_id: Option<i64>,
2413 #[arg(long)]
2414 pub repository_owner: String,
2415 #[arg(long)]
2416 pub repository_name: String,
2417 #[arg(long)]
2418 pub repository_full_name: String,
2419 #[arg(long)]
2420 pub is_private: bool,
2421 #[command(flatten)]
2422 pub target: ApiTargetOptions,
2423}
2424
2425#[derive(Args, Debug)]
2426pub struct ApiRunnersInventoryListCmd {
2427 #[arg(long)]
2428 pub organization_id: String,
2429 #[command(flatten)]
2430 pub target: ApiTargetOptions,
2431}
2432
2433#[derive(Args, Debug)]
2434pub struct ApiRunnersInventoryUpsertCmd {
2435 #[arg(long)]
2436 pub organization_id: String,
2437 #[arg(long)]
2438 pub runner_host_id: Option<String>,
2439 #[arg(long)]
2440 pub runner_group_id: Option<String>,
2441 #[arg(long)]
2442 pub github_runner_id: Option<i64>,
2443 #[arg(long)]
2444 pub name: String,
2445 #[arg(long)]
2446 pub platform: String,
2447 #[arg(long)]
2448 pub os: Option<String>,
2449 #[arg(long)]
2450 pub architecture: Option<String>,
2451 #[arg(long)]
2452 pub status: Option<String>,
2453 #[arg(long)]
2454 pub busy: bool,
2455 #[arg(long)]
2456 pub labels_json: Option<String>,
2457 #[arg(long)]
2458 pub metadata_json: Option<String>,
2459 #[command(flatten)]
2460 pub target: ApiTargetOptions,
2461}
2462
2463#[derive(Args, Debug, Clone)]
2464pub struct ApiRunnersJobsListCmd {
2465 #[arg(long)]
2466 pub organization_id: Option<String>,
2467 #[arg(long)]
2468 pub runner_host_id: Option<String>,
2469 #[arg(long)]
2470 pub runner_id: Option<String>,
2471 #[arg(long)]
2472 pub daemon_id: Option<String>,
2473 #[arg(long)]
2474 pub status: Option<String>,
2475 #[arg(long)]
2476 pub phase: Option<String>,
2477 #[arg(long)]
2478 pub limit: Option<usize>,
2479 #[command(flatten)]
2480 pub target: ApiTargetOptions,
2481}
2482
2483#[derive(Args, Debug)]
2484pub struct ApiRunnersJobsCreateCmd {
2485 #[arg(long)]
2486 pub organization_id: String,
2487 #[arg(long)]
2488 pub runner_host_id: Option<String>,
2489 #[arg(long)]
2490 pub daemon_id: Option<String>,
2491 #[arg(long)]
2492 pub runner_id: Option<String>,
2493 #[arg(long)]
2494 pub job_kind: String,
2495 #[arg(long)]
2496 pub priority: Option<i32>,
2497 #[arg(long)]
2498 pub max_attempts: Option<i32>,
2499 #[arg(long)]
2500 pub run_after: Option<String>,
2501 #[arg(long)]
2502 pub payload_json: Option<String>,
2503 #[command(flatten)]
2504 pub target: ApiTargetOptions,
2505}
2506
2507#[derive(Args, Debug)]
2508pub struct ApiRunnersJobsClaimCmd {
2509 #[arg(long)]
2510 pub runner_host_id: Option<String>,
2511 #[arg(long)]
2512 pub daemon_id: Option<String>,
2513 #[arg(long)]
2514 pub locked_by: Option<String>,
2515 #[command(flatten)]
2516 pub target: ApiTargetOptions,
2517}
2518
2519#[derive(Args, Debug)]
2520pub struct ApiRunnersJobsUpdateCmd {
2521 #[arg(help = "Runner job ID")]
2522 pub runner_job_id: String,
2523 #[arg(long)]
2524 pub status: String,
2525 #[arg(long)]
2526 pub error_text: Option<String>,
2527 #[command(flatten)]
2528 pub target: ApiTargetOptions,
2529}
2530
2531#[derive(Subcommand, Debug)]
2532pub enum ApiJobsSubCommand {
2533 #[command(about = "List deployment jobs")]
2534 List(ApiJobsListCmd),
2535 #[command(about = "Create a deployment job for a project")]
2536 Create(ApiJobsCreateCmd),
2537 #[command(about = "Claim the next deployment job for a daemon")]
2538 Claim(ApiJobsClaimCmd),
2539 #[command(about = "Update deployment job status")]
2540 Update(ApiJobsUpdateCmd),
2541}
2542
2543#[derive(Args, Debug)]
2544pub struct ApiJobsListCmd {
2545 #[arg(long, help = "Optional project ID filter")]
2546 pub project_id: Option<String>,
2547 #[arg(long, help = "Optional deployment ID filter")]
2548 pub deployment_id: Option<String>,
2549 #[arg(long, help = "Optional daemon ID filter")]
2550 pub daemon_id: Option<String>,
2551 #[arg(long, help = "Optional status filter")]
2552 pub status: Option<String>,
2553 #[arg(long, help = "Optional result limit")]
2554 pub limit: Option<usize>,
2555 #[command(flatten)]
2556 pub target: ApiTargetOptions,
2557}
2558
2559#[derive(Args, Debug)]
2560pub struct ApiJobsCreateCmd {
2561 #[arg(long, help = "Project ID")]
2562 pub project_id: String,
2563 #[arg(long, help = "Deployment ID")]
2564 pub deployment_id: String,
2565 #[arg(long, help = "Optional daemon ID assignment")]
2566 pub daemon_id: Option<String>,
2567 #[arg(long, help = "Optional priority")]
2568 pub priority: Option<i32>,
2569 #[arg(long, help = "Optional max attempts")]
2570 pub max_attempts: Option<i32>,
2571 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
2572 pub run_after: Option<String>,
2573 #[arg(long, help = "Optional payload JSON object")]
2574 pub payload_json: Option<String>,
2575 #[command(flatten)]
2576 pub target: ApiTargetOptions,
2577}
2578
2579#[derive(Args, Debug)]
2580pub struct ApiJobsClaimCmd {
2581 #[arg(long, help = "Daemon ID claiming work")]
2582 pub daemon_id: String,
2583 #[arg(long, help = "Optional lock owner")]
2584 pub locked_by: Option<String>,
2585 #[command(flatten)]
2586 pub target: ApiTargetOptions,
2587}
2588
2589#[derive(Args, Debug)]
2590pub struct ApiJobsUpdateCmd {
2591 #[arg(help = "Deployment job ID")]
2592 pub job_id: String,
2593 #[arg(long, help = "Deployment job status enum value")]
2594 pub status: String,
2595 #[arg(long, help = "Optional error text")]
2596 pub error_text: Option<String>,
2597 #[command(flatten)]
2598 pub target: ApiTargetOptions,
2599}
2600
2601#[derive(Args, Debug)]
2602pub struct ApiRoutesCmd {
2603 #[command(subcommand)]
2604 pub command: ApiRoutesSubCommand,
2605}
2606
2607#[derive(Subcommand, Debug)]
2608pub enum ApiRoutesSubCommand {
2609 #[command(about = "List configured proxy routes")]
2610 List(ApiRoutesListCmd),
2611 #[command(about = "Create or replace a proxy route")]
2612 Create(ApiRoutesCreateCmd),
2613 #[command(about = "Delete a proxy route by domain")]
2614 Delete(ApiRoutesDeleteCmd),
2615}
2616
2617#[derive(Args, Debug)]
2618pub struct ApiRoutesListCmd {
2619 #[command(flatten)]
2620 pub target: ApiTargetOptions,
2621}
2622
2623#[derive(Args, Debug)]
2624pub struct ApiRoutesCreateCmd {
2625 #[arg(long, help = "Domain name for the route")]
2626 pub domain: String,
2627 #[arg(long, help = "Upstream target URL", required = true)]
2628 pub target: Vec<String>,
2629 #[arg(
2630 long,
2631 help = "Weighted upstream target in url=weight form",
2632 value_name = "URL=WEIGHT"
2633 )]
2634 pub weighted_target: Vec<String>,
2635 #[arg(long, help = "Optional header condition")]
2636 pub header_condition: Option<String>,
2637 #[arg(long, help = "Optional path prefix condition")]
2638 pub path_prefix: Option<String>,
2639 #[command(flatten)]
2640 pub target_options: ApiTargetOptions,
2641}
2642
2643#[derive(Args, Debug)]
2644pub struct ApiRoutesDeleteCmd {
2645 #[arg(help = "Domain name for the route")]
2646 pub domain: String,
2647 #[command(flatten)]
2648 pub target: ApiTargetOptions,
2649}
2650
2651#[derive(Args, Debug)]
2652pub struct ApiRequestCmd {
2653 #[arg(help = "Request path like /projects or a full https:// URL")]
2654 pub path: String,
2655 #[arg(
2656 short = 'X',
2657 long,
2658 help = "HTTP method to use (default: GET, or POST when a body is provided)"
2659 )]
2660 pub method: Option<String>,
2661 #[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
2662 pub body: Option<String>,
2663 #[arg(long, help = "Read the request body from a file")]
2664 pub body_file: Option<PathBuf>,
2665 #[command(flatten)]
2666 pub target: ApiTargetOptions,
2667}
2668#[derive(Args, Debug)]
2669pub struct TailCmd {
2670 #[arg(long, help = "Tail Kafka topic instead of log files")]
2671 pub kafka: bool,
2672 #[arg(long, help = "Ship logs to Kafka")]
2673 pub ship: bool,
2674}
2675
2676#[derive(Args, Debug)]
2677#[command(
2678 arg_required_else_help = true,
2679 disable_help_subcommand = true,
2680 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2681 after_help = crate::cli::help_render::GENERATE_AFTER_HELP
2682)]
2683pub struct GenerateCmd {
2684 #[command(subcommand)]
2685 pub command: GenerateSubCommand,
2686}
2687
2688#[derive(Subcommand, Debug)]
2689pub enum GenerateSubCommand {
2690 #[command(about = "Generate or update .xbp/xbp.yaml (and convert legacy JSON)")]
2691 Config(GenerateConfigCmd),
2692 Systemd(GenerateSystemdCmd),
2693}
2694
2695#[derive(Args, Debug)]
2696pub struct GenerateConfigCmd {
2697 #[arg(
2698 long,
2699 help = "Overwrite .xbp/xbp.yaml if it already exists (default errors when present)"
2700 )]
2701 pub force: bool,
2702 #[arg(
2703 long,
2704 help = "Refresh .xbp/xbp.yaml by applying project detection defaults for missing fields"
2705 )]
2706 pub update: bool,
2707 #[arg(
2708 long,
2709 help = "Path to a legacy xbp.json file to convert into .xbp/xbp.yaml"
2710 )]
2711 pub from_json: Option<PathBuf>,
2712}
2713
2714#[derive(Args, Debug)]
2715#[command(
2716 arg_required_else_help = true,
2717 disable_help_subcommand = true,
2718 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2719 after_help = "Examples:\n xbp workers list\n xbp workers list --failed -n 10\n xbp workers ls\n xbp workers logs -f\n xbp workers logs --build --failed -n 200 -o build.log\n xbp workers logs --build --grep error --errors-only\n xbp workers secrets put --environment production --name GITHUB_APP_PRIVATE_KEY --from-stdin\n xbp workers secrets list --environment production\n xbp workers settings get --environment production\n xbp workers wrangler generate-config --output wrangler.deploy.json\n xbp workers d1 migrations apply DB --remote\n xbp workers deploy configure --write-config\n xbp workers deploy run --app athena-studio\n xbp workers deploy ci --app athena --all\n xbp workers deploy sync-env-local\n xbp workers deploy ci --version-upload\n xbp workers worktree link-dev-vars"
2720)]
2721pub struct WorkersCmd {
2722 #[arg(
2723 long,
2724 help = "Workers project root (defaults to current dir, or apps/web inside the current XBP repo when present)"
2725 )]
2726 pub root: Option<PathBuf>,
2727 #[arg(
2728 long,
2729 help = "Worker app name from .xbp/xbp.yaml `workers:` (defaults to the app matching the current directory)"
2730 )]
2731 pub app: Option<String>,
2732 #[arg(long, help = "Cloudflare API token override")]
2733 pub token: Option<String>,
2734 #[arg(long, help = "Cloudflare account ID override")]
2735 pub account_id: Option<String>,
2736 #[command(subcommand)]
2737 pub command: WorkersSubCommand,
2738}
2739
2740#[derive(Subcommand, Debug)]
2741pub enum WorkersSubCommand {
2742 #[command(
2743 alias = "ls",
2744 about = "List Cloudflare Worker scripts with build and placement status",
2745 after_help = "Examples:\n xbp workers list\n xbp workers list --all\n xbp workers list --failed -n 10\n xbp workers list --status running --sort modified\n xbp workers list --json"
2746 )]
2747 List(WorkersListCmd),
2748 #[command(
2749 about = "Show deployment status, stream runtime logs, or fetch Workers Builds CI logs",
2750 after_help = "Examples:\n xbp workers logs --worker suits-formations --json\n xbp workers logs -f\n xbp workers logs -n 100 -o runtime.log\n xbp workers logs xbp-production\n xbp workers logs --build --wait\n xbp workers logs --build --failed -n 200\n xbp workers logs --build --grep error --errors-only\n xbp workers logs --build --list-builds"
2751 )]
2752 Logs(WorkersLogsCmd),
2753 #[command(about = "Manage secret bindings for a Worker script or Wrangler environment")]
2754 Secrets(WorkersSecretsCmd),
2755 #[command(about = "Fetch remote Worker settings via the Cloudflare API")]
2756 Settings(WorkersSettingsCmd),
2757 #[command(about = "Inspect or generate Wrangler config helpers")]
2758 Wrangler(WorkersWranglerCmd),
2759 #[command(about = "Run Wrangler D1 migration helpers")]
2760 D1(WorkersD1Cmd),
2761 #[command(about = "Run Worker deploy and predeploy helpers")]
2762 Deploy(WorkersDeployCmd),
2763 #[command(about = "Inspect shared worktree paths or link shared dev files")]
2764 Worktree(WorkersWorktreeCmd),
2765 #[command(about = "Show resolved Worker runtime metadata from local env and config files")]
2766 Env(WorkersEnvCmd),
2767}
2768
2769#[derive(Args, Debug, Default)]
2770pub struct WorkersListCmd {
2771 #[arg(
2772 long,
2773 help = "Show every Worker in the account instead of only Workers for this XBP project"
2774 )]
2775 pub all: bool,
2776 #[arg(long, help = "Output the worker inventory as JSON")]
2777 pub json: bool,
2778 #[arg(short = 'n', long = "limit", help = "Show at most N workers")]
2779 pub limit: Option<usize>,
2780 #[arg(
2781 long = "status",
2782 help = "Filter by build status (failed, success, running, unknown)"
2783 )]
2784 pub status: Option<String>,
2785 #[arg(long, help = "Shortcut for --status failed")]
2786 pub failed: bool,
2787 #[arg(
2788 long = "sort",
2789 default_value = "name",
2790 help = "Sort workers by name, modified, or status"
2791 )]
2792 pub sort: String,
2793 #[arg(long = "no-color", help = "Disable ANSI colors in table output")]
2794 pub no_color: bool,
2795}
2796
2797#[derive(Args, Debug)]
2798pub struct WorkersLogsCmd {
2799 #[command(flatten)]
2800 pub target: WorkersTargetArgs,
2801 #[arg(
2802 short = 'f',
2803 long = "follow",
2804 help = "Stream runtime logs until interrupted (wrangler tail)"
2805 )]
2806 pub follow: bool,
2807 #[arg(
2808 long = "build",
2809 help = "Show Workers Builds CI logs instead of runtime deployment output"
2810 )]
2811 pub build: bool,
2812 #[arg(
2813 long = "failed",
2814 help = "When using --build, prefer the latest failed build"
2815 )]
2816 pub failed: bool,
2817 #[arg(long, help = "Output logs as JSON")]
2818 pub json: bool,
2819 #[arg(
2820 short = 'n',
2821 long = "lines",
2822 help = "Show only the last N log lines (deployments/build output)"
2823 )]
2824 pub lines: Option<usize>,
2825 #[arg(
2826 short = 'o',
2827 long = "output",
2828 help = "Write log output to a file instead of stdout"
2829 )]
2830 pub output: Option<PathBuf>,
2831 #[arg(
2832 short = 'g',
2833 long = "grep",
2834 help = "Filter log lines matching this pattern (case-insensitive substring)"
2835 )]
2836 pub grep: Option<String>,
2837 #[arg(
2838 short = 'e',
2839 long = "errors-only",
2840 help = "Show only lines that look like errors or failures"
2841 )]
2842 pub errors_only: bool,
2843 #[arg(long = "no-color", help = "Disable ANSI colors in log output")]
2844 pub no_color: bool,
2845 #[arg(
2846 long = "list-builds",
2847 help = "List recent Workers Builds before fetching logs (with --build)"
2848 )]
2849 pub list_builds: bool,
2850 #[arg(
2851 long = "build-index",
2852 help = "Select build by index from --list-builds (0 = latest)"
2853 )]
2854 pub build_index: Option<usize>,
2855 #[arg(
2856 long,
2857 help = "Poll until the selected Workers Build finishes before fetching logs"
2858 )]
2859 pub wait: bool,
2860 #[arg(long = "no-wait", help = "Skip polling for in-progress Workers Builds")]
2861 pub no_wait: bool,
2862 #[arg(
2863 long = "wait-seconds",
2864 default_value_t = 120,
2865 help = "Maximum seconds to poll an in-progress Workers Build"
2866 )]
2867 pub wait_seconds: u64,
2868 #[arg(help = "Worker script name. When omitted, xbp prompts interactively.")]
2869 pub script_name: Option<String>,
2870}
2871
2872#[derive(Args, Debug, Clone, Default)]
2873pub struct WorkersTargetArgs {
2874 #[arg(
2875 long,
2876 help = "Worker base name (defaults to wrangler config name or xbp)"
2877 )]
2878 pub worker: Option<String>,
2879 #[arg(
2880 long = "environment",
2881 alias = "env",
2882 help = "Wrangler environment name. The remote script resolves to <worker>-<environment>."
2883 )]
2884 pub environment: Option<String>,
2885 #[arg(
2886 long,
2887 help = "Exact remote script name override. When set, this bypasses <worker>-<environment> resolution."
2888 )]
2889 pub script: Option<String>,
2890}
2891
2892#[derive(Args, Debug)]
2893pub struct WorkersSecretsCmd {
2894 #[command(flatten)]
2895 pub target: WorkersTargetArgs,
2896 #[command(subcommand)]
2897 pub command: WorkersSecretsSubCommand,
2898}
2899
2900#[derive(Subcommand, Debug)]
2901pub enum WorkersSecretsSubCommand {
2902 #[command(about = "List secret bindings on the resolved Worker script")]
2903 List(WorkersSecretsListCmd),
2904 #[command(about = "Fetch one secret binding metadata or value")]
2905 Get(WorkersSecretsGetCmd),
2906 #[command(about = "Create or update a secret binding")]
2907 Put(WorkersSecretsPutCmd),
2908 #[command(about = "Delete a secret binding")]
2909 Delete(WorkersSecretsDeleteCmd),
2910 #[command(about = "Create, update, or delete multiple secret bindings from a file")]
2911 Bulk(WorkersSecretsBulkCmd),
2912}
2913
2914#[derive(Args, Debug, Default)]
2915pub struct WorkersSecretsListCmd {}
2916
2917#[derive(Args, Debug)]
2918pub struct WorkersSecretsGetCmd {
2919 #[arg(long, help = "Secret binding name")]
2920 pub name: String,
2921}
2922
2923#[derive(Args, Debug)]
2924pub struct WorkersSecretsPutCmd {
2925 #[arg(long, help = "Secret binding name")]
2926 pub name: String,
2927 #[arg(long, help = "Secret value")]
2928 pub value: Option<String>,
2929 #[arg(long, help = "Read the secret value from stdin instead of --value")]
2930 pub from_stdin: bool,
2931}
2932
2933#[derive(Args, Debug)]
2934pub struct WorkersSecretsDeleteCmd {
2935 #[arg(long, help = "Secret binding name")]
2936 pub name: String,
2937}
2938
2939#[derive(Args, Debug)]
2940pub struct WorkersSecretsBulkCmd {
2941 #[arg(long, help = "Path to a .env or JSON file containing secret updates")]
2942 pub file: PathBuf,
2943 #[arg(
2944 long,
2945 default_value = "env",
2946 help = "Input format: env or json. For json, pass an object mapping names to string values or null for deletes."
2947 )]
2948 pub format: String,
2949}
2950
2951#[derive(Args, Debug)]
2952pub struct WorkersSettingsCmd {
2953 #[command(flatten)]
2954 pub target: WorkersTargetArgs,
2955}
2956
2957#[derive(Args, Debug)]
2958pub struct WorkersWranglerCmd {
2959 #[command(subcommand)]
2960 pub command: WorkersWranglerSubCommand,
2961}
2962
2963#[derive(Subcommand, Debug)]
2964pub enum WorkersWranglerSubCommand {
2965 #[command(about = "Generate a Wrangler deploy config JSON file from env vars")]
2966 GenerateConfig(WorkersWranglerGenerateConfigCmd),
2967 #[command(about = "Resolve which Wrangler config file local dev should use")]
2968 ConfigPath(WorkersWranglerConfigPathCmd),
2969 #[command(
2970 name = "run",
2971 alias = "exec",
2972 about = "Run any official Wrangler command from the selected Worker root",
2973 after_help = "Pass Wrangler arguments after `--`. Examples:\n xbp workers wrangler run -- d1 list\n xbp workers wrangler run -- d1 execute DB --remote --file schema.sql\n xbp workers wrangler run -- r2 bucket create r2-bucket\n xbp workers wrangler run -- r2 bucket cors set r2-bucket --file cors.json\n xbp workers wrangler run -- queues create xbp-events\n xbp workers wrangler run -- secrets-store store list --remote"
2974 )]
2975 Run(WorkersWranglerRunCmd),
2976}
2977
2978#[derive(Args, Debug)]
2979pub struct WorkersWranglerGenerateConfigCmd {
2980 #[arg(
2981 long,
2982 default_value = "wrangler.deploy.json",
2983 help = "Output filename, relative to the worker root unless absolute"
2984 )]
2985 pub output: PathBuf,
2986}
2987
2988#[derive(Args, Debug)]
2989pub struct WorkersWranglerConfigPathCmd {
2990 #[arg(
2991 long,
2992 default_value = "serve",
2993 help = "Calling command name, for example serve"
2994 )]
2995 pub command_name: String,
2996 #[arg(
2997 long,
2998 default_value = "development",
2999 help = "Execution mode, for example development or production"
3000 )]
3001 pub mode: String,
3002}
3003
3004#[derive(Args, Debug)]
3005pub struct WorkersWranglerRunCmd {
3006 #[arg(
3007 required = true,
3008 trailing_var_arg = true,
3009 allow_hyphen_values = true,
3010 help = "Arguments passed verbatim to the official Wrangler CLI"
3011 )]
3012 pub args: Vec<String>,
3013}
3014
3015#[derive(Args, Debug)]
3016pub struct WorkersD1Cmd {
3017 #[command(subcommand)]
3018 pub command: WorkersD1SubCommand,
3019}
3020
3021#[derive(Subcommand, Debug)]
3022pub enum WorkersD1SubCommand {
3023 #[command(about = "Apply pending Wrangler D1 migrations")]
3024 Migrations(WorkersD1MigrationsCmd),
3025}
3026
3027#[derive(Args, Debug)]
3028pub struct WorkersD1MigrationsCmd {
3029 #[command(subcommand)]
3030 pub command: WorkersD1MigrationsSubCommand,
3031}
3032
3033#[derive(Subcommand, Debug)]
3034pub enum WorkersD1MigrationsSubCommand {
3035 #[command(about = "Apply pending migrations to a local or remote D1 database")]
3036 Apply(WorkersD1MigrationsApplyCmd),
3037}
3038
3039#[derive(Args, Debug)]
3040pub struct WorkersD1MigrationsApplyCmd {
3041 #[arg(help = "D1 database binding or name, for example DB")]
3042 pub database: String,
3043 #[arg(
3044 long,
3045 conflicts_with = "remote",
3046 help = "Apply migrations to the local Wrangler D1 database"
3047 )]
3048 pub local: bool,
3049 #[arg(
3050 long,
3051 conflicts_with = "local",
3052 help = "Apply migrations to the remote D1 database"
3053 )]
3054 pub remote: bool,
3055 #[arg(long, help = "Wrangler config path override")]
3056 pub config: Option<PathBuf>,
3057 #[command(flatten)]
3058 pub target: WorkersTargetArgs,
3059 #[arg(
3060 long,
3061 help = "Persist local D1 state to this directory. When omitted in a git worktree, xbp uses the shared .wrangler/state path automatically."
3062 )]
3063 pub persist_to: Option<PathBuf>,
3064 #[arg(
3065 long,
3066 help = "Disable the automatic shared .wrangler/state path when running local migrations from a git worktree"
3067 )]
3068 pub no_shared_worktree_state: bool,
3069}
3070
3071#[derive(Args, Debug)]
3072pub struct WorkersDeployCmd {
3073 #[arg(
3074 long,
3075 help = "Run the deploy action for every worker app configured in .xbp/xbp.yaml"
3076 )]
3077 pub all: bool,
3078 #[command(subcommand)]
3079 pub command: WorkersDeploySubCommand,
3080}
3081
3082#[derive(Subcommand, Debug)]
3083pub enum WorkersDeploySubCommand {
3084 #[command(
3085 about = "Discover worker apps, sync Wrangler config, and optionally write .xbp/xbp.yaml"
3086 )]
3087 Configure(WorkersDeployConfigureCmd),
3088 #[command(about = "Run the configured deploy command for one or more worker apps")]
3089 Run(WorkersDeployRunCmd),
3090 #[command(about = "Run the predeploy sync flow unless Workers CI mode is active")]
3091 Predeploy(WorkersDeployPredeployCmd),
3092 #[command(about = "Read .env.local or process env, then emit .dev.vars and Wrangler configs")]
3093 SyncEnvLocal(WorkersDeploySyncEnvLocalCmd),
3094 #[command(
3095 about = "Run the built-in Cloudflare Worker CI deploy workflow (build + wrangler deploy)"
3096 )]
3097 Ci(WorkersDeployCiCmd),
3098 #[command(
3099 about = "Run the existing deploy-selection flow that chooses CI or local deploy behavior"
3100 )]
3101 Select(WorkersDeploySelectCmd),
3102}
3103
3104#[derive(Args, Debug, Default)]
3105pub struct WorkersDeployConfigureCmd {
3106 #[arg(
3107 long,
3108 help = "Scan the repo for Wrangler projects and merge them into .xbp/xbp.yaml `workers:`"
3109 )]
3110 pub write_config: bool,
3111 #[arg(
3112 long,
3113 help = "Preview worker discovery and config writes without changing files"
3114 )]
3115 pub dry_run: bool,
3116}
3117
3118#[derive(Args, Debug, Default)]
3119pub struct WorkersDeployRunCmd {}
3120
3121#[derive(Args, Debug)]
3122pub struct WorkersDeployPredeployCmd {
3123 #[arg(long, help = "Force Workers CI mode and skip local sync")]
3124 pub ci: bool,
3125}
3126
3127#[derive(Args, Debug)]
3128pub struct WorkersDeploySyncEnvLocalCmd {}
3129
3130#[derive(Args, Debug)]
3131pub struct WorkersDeployCiCmd {
3132 #[arg(long, help = "Upload a new version without immediately deploying it")]
3133 pub version_upload: bool,
3134}
3135
3136#[derive(Args, Debug)]
3137pub struct WorkersDeploySelectCmd {
3138 #[arg(long, help = "Force the WORKERS_CI=1 branch of the selector")]
3139 pub ci: bool,
3140 #[arg(
3141 long,
3142 help = "Branch name to expose as WORKERS_CI_BRANCH when --ci is set"
3143 )]
3144 pub branch: Option<String>,
3145}
3146
3147#[derive(Args, Debug)]
3148pub struct WorkersWorktreeCmd {
3149 #[command(subcommand)]
3150 pub command: WorkersWorktreeSubCommand,
3151}
3152
3153#[derive(Subcommand, Debug)]
3154pub enum WorkersWorktreeSubCommand {
3155 #[command(about = "Print repo-root, primary worktree, and shared Wrangler state paths")]
3156 Paths(WorkersWorktreePathsCmd),
3157 #[command(
3158 about = "Symlink apps/web/.dev.vars and wrangler.dev.jsonc from the primary worktree when in a linked worktree"
3159 )]
3160 LinkDevVars(WorkersWorktreeLinkDevVarsCmd),
3161}
3162
3163#[derive(Args, Debug, Default)]
3164pub struct WorkersWorktreePathsCmd {}
3165
3166#[derive(Args, Debug, Default)]
3167pub struct WorkersWorktreeLinkDevVarsCmd {}
3168
3169#[derive(Args, Debug)]
3170pub struct WorkersEnvCmd {
3171 #[command(flatten)]
3172 pub target: WorkersTargetArgs,
3173 #[arg(
3174 long,
3175 help = "Show resolved plain-text binding values instead of masking them"
3176 )]
3177 pub show_values: bool,
3178}
3179
3180#[derive(Args, Debug)]
3181#[command(
3182 arg_required_else_help = true,
3183 disable_help_subcommand = true,
3184 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3185 after_help = "Examples:\n xbp cloudflare doctor --app auth\n xbp cloudflare init --app auth --worker-root apps/auth-worker --container-port 8787 --health-path /health --write\n xbp cloudflare deploy --app auth --rollout immediate --dry-run\n xbp cloudflare release --app auth --version 1.2.3 --rollout gradual\n xbp cloudflare containers instances --app auth\n xbp cloudflare jobs enqueue deploy --app auth --rollout immediate"
3186)]
3187pub struct CloudflareCmd {
3188 #[arg(
3189 long,
3190 global = true,
3191 help = "XBP project root (defaults to the nearest directory containing .xbp/xbp.yaml)"
3192 )]
3193 pub root: Option<PathBuf>,
3194 #[arg(
3195 long,
3196 global = true,
3197 help = "Worker app name from .xbp/xbp.yaml `workers:`"
3198 )]
3199 pub app: Option<String>,
3200 #[arg(long, global = true, help = "Cloudflare API token override")]
3201 pub token: Option<String>,
3202 #[arg(long, global = true, help = "Cloudflare account ID override")]
3203 pub account_id: Option<String>,
3204 #[command(subcommand)]
3205 pub command: CloudflareSubCommand,
3206}
3207
3208#[derive(Subcommand, Debug)]
3209pub enum CloudflareSubCommand {
3210 #[command(
3211 about = "Validate Wrangler, container metadata, bindings, secrets, and deploy readiness"
3212 )]
3213 Doctor(CloudflareDoctorCmd),
3214 #[command(about = "Discover or write the project-level Worker container contract")]
3215 Init(CloudflareInitCmd),
3216 #[command(about = "Run the canonical Worker + Container deploy workflow")]
3217 Deploy(CloudflareDeployCmd),
3218 #[command(about = "Sync a version-bearing deploy var, deploy, and verify live health")]
3219 Release(CloudflareReleaseCmd),
3220 #[command(about = "Wrap official Wrangler container commands from the selected app root")]
3221 Containers(CloudflareContainersCmd),
3222 #[command(
3223 about = "Manage Cloudflare Tunnels through official Wrangler commands",
3224 after_help = "Tunnel commands are experimental in Wrangler. Examples:\n xbp cloudflare tunnel create my-app\n xbp cloudflare tunnel list\n xbp cloudflare tunnel info my-app\n xbp cloudflare tunnel run my-app\n xbp cloudflare tunnel run --token <TOKEN>\n xbp cloudflare tunnel quick-start http://localhost:8080\n xbp cloudflare tunnel delete my-app --force"
3225 )]
3226 Tunnel(CloudflareTunnelCmd),
3227 #[command(
3228 about = "Manage Cloudflare Workflows through official Wrangler commands",
3229 after_help = "Pass the documented Wrangler workflows command after `workflows`. Examples:\n xbp cloudflare workflows list\n xbp cloudflare workflows describe my-workflow\n xbp cloudflare workflows trigger my-workflow '{\"key\":\"value\"}'\n xbp cloudflare workflows instances list my-workflow --local\n xbp cloudflare workflows instances describe my-workflow latest\n xbp cloudflare workflows instances send-event my-workflow latest --type my-event --payload '{\"key\":\"value\"}'\n xbp cloudflare workflows instances pause my-workflow latest\n xbp cloudflare workflows instances resume my-workflow latest\n xbp cloudflare workflows instances restart my-workflow latest\n xbp cloudflare workflows instances terminate my-workflow latest"
3230 )]
3231 Workflows(CloudflareWorkflowsCmd),
3232 #[command(
3233 about = "Run Wrangler local-development commands with environment-specific vars",
3234 after_help = "Pass the documented Wrangler local-development command after `env`. Examples:\n xbp cloudflare env dev\n xbp cloudflare env dev --env staging\n xbp cloudflare env dev --env staging --port 8787"
3235 )]
3236 Env(CloudflareEnvCmd),
3237 #[command(about = "Manage local file-backed Cloudflare workflow jobs")]
3238 Jobs(CloudflareJobsCmd),
3239}
3240
3241#[derive(Args, Debug, Default)]
3242pub struct CloudflareDoctorCmd {
3243 #[arg(
3244 long,
3245 help = "Skip live Wrangler calls and only validate local files/config"
3246 )]
3247 pub offline: bool,
3248}
3249
3250#[derive(Args, Debug)]
3251pub struct CloudflareInitCmd {
3252 #[arg(long, help = "Worker app root to store in .xbp/xbp.yaml")]
3253 pub worker_root: PathBuf,
3254 #[arg(long, help = "Container port exposed by the runtime")]
3255 pub container_port: u16,
3256 #[arg(
3257 long,
3258 default_value = "/health",
3259 help = "Container health endpoint path"
3260 )]
3261 pub health_path: String,
3262 #[arg(long, help = "Container Durable Object class name")]
3263 pub class_name: Option<String>,
3264 #[arg(long, help = "Worker binding name for the container Durable Object")]
3265 pub binding: Option<String>,
3266 #[arg(
3267 long,
3268 default_value = "Dockerfile",
3269 help = "Dockerfile path relative to the Worker root"
3270 )]
3271 pub dockerfile: PathBuf,
3272 #[arg(long, help = "Cloudflare container application id")]
3273 pub application_id: Option<String>,
3274 #[arg(
3275 long = "required-secret",
3276 help = "Required secret/env binding name",
3277 value_name = "NAME"
3278 )]
3279 pub required_secrets: Vec<String>,
3280 #[arg(long, help = "Wrangler vars key that should carry release versions")]
3281 pub version_var: Option<String>,
3282 #[arg(
3283 long,
3284 help = "Persist the discovered/scaffolded contract to .xbp/xbp.yaml"
3285 )]
3286 pub write: bool,
3287}
3288
3289#[derive(Args, Debug)]
3290pub struct CloudflareDeployCmd {
3291 #[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
3292 pub rollout: CloudflareRollout,
3293 #[arg(long, help = "Run validation and Wrangler deploy --dry-run only")]
3294 pub dry_run: bool,
3295 #[arg(
3296 long,
3297 help = "Run post-deploy verification without invoking Wrangler deploy"
3298 )]
3299 pub skip_deploy: bool,
3300 #[arg(
3301 long,
3302 help = "Allow the container image tag to remain unchanged after deploy verification"
3303 )]
3304 pub allow_unchanged_container_image: bool,
3305 #[arg(
3306 long,
3307 help = "Delete old container image tags after successful verification"
3308 )]
3309 pub prune_old_images: bool,
3310 #[arg(long, help = "Image tag count to retain when pruning old tags")]
3311 pub keep_image_tag_count: Option<usize>,
3312}
3313
3314#[derive(Args, Debug)]
3315pub struct CloudflareReleaseCmd {
3316 #[arg(long, help = "Release version to write to the configured version var")]
3317 pub version: String,
3318 #[arg(
3319 long,
3320 help = "Release domain that must validate before this container-backed Worker release"
3321 )]
3322 pub domain: Option<String>,
3323 #[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
3324 pub rollout: CloudflareRollout,
3325 #[arg(
3326 long,
3327 help = "Run release verification without invoking Wrangler deploy"
3328 )]
3329 pub skip_deploy: bool,
3330 #[arg(
3331 long,
3332 help = "Allow the container image tag to remain unchanged after release verification"
3333 )]
3334 pub allow_unchanged_container_image: bool,
3335 #[arg(
3336 long,
3337 help = "Delete old container image tags after successful verification"
3338 )]
3339 pub prune_old_images: bool,
3340 #[arg(long, help = "Image tag count to retain when pruning old tags")]
3341 pub keep_image_tag_count: Option<usize>,
3342}
3343
3344#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
3345#[serde(rename_all = "kebab-case")]
3346pub enum CloudflareRollout {
3347 Immediate,
3348 Gradual,
3349 None,
3350}
3351
3352impl CloudflareRollout {
3353 pub fn as_str(self) -> &'static str {
3354 match self {
3355 Self::Immediate => "immediate",
3356 Self::Gradual => "gradual",
3357 Self::None => "none",
3358 }
3359 }
3360}
3361
3362#[derive(Args, Debug)]
3363pub struct CloudflareContainersCmd {
3364 #[command(subcommand)]
3365 pub command: CloudflareContainersSubCommand,
3366}
3367
3368#[derive(Args, Debug)]
3369pub struct CloudflareTunnelCmd {
3370 #[command(subcommand)]
3371 pub command: CloudflareTunnelSubCommand,
3372}
3373
3374#[derive(Subcommand, Debug)]
3375pub enum CloudflareTunnelSubCommand {
3376 #[command(about = "Create a remotely managed Cloudflare Tunnel")]
3377 Create(CloudflareTunnelCreateCmd),
3378 #[command(about = "Delete a Cloudflare Tunnel")]
3379 Delete(CloudflareTunnelDeleteCmd),
3380 #[command(about = "Show details for a Cloudflare Tunnel")]
3381 Info(CloudflareTunnelInfoCmd),
3382 #[command(about = "List Cloudflare Tunnels")]
3383 List(CloudflareTunnelListCmd),
3384 #[command(about = "Run a named or token-authenticated Cloudflare Tunnel")]
3385 Run(CloudflareTunnelRunCmd),
3386 #[command(about = "Start a temporary anonymous Quick Tunnel")]
3387 QuickStart(CloudflareTunnelQuickStartCmd),
3388}
3389
3390#[derive(Args, Debug)]
3391pub struct CloudflareTunnelCreateCmd {
3392 #[arg(help = "Unique tunnel name within the Cloudflare account")]
3393 pub name: String,
3394}
3395
3396#[derive(Args, Debug)]
3397pub struct CloudflareTunnelDeleteCmd {
3398 #[arg(help = "Tunnel name or UUID")]
3399 pub tunnel: String,
3400 #[arg(long, help = "Skip the confirmation prompt")]
3401 pub force: bool,
3402}
3403
3404#[derive(Args, Debug)]
3405pub struct CloudflareTunnelInfoCmd {
3406 #[arg(help = "Tunnel name or UUID")]
3407 pub tunnel: String,
3408}
3409
3410#[derive(Args, Debug, Default)]
3411pub struct CloudflareTunnelListCmd {}
3412
3413#[derive(Args, Debug)]
3414pub struct CloudflareTunnelRunCmd {
3415 #[arg(help = "Tunnel name or UUID; omit when using --token")]
3416 pub tunnel: Option<String>,
3417 #[arg(long, help = "Tunnel token; avoids API lookup and authentication")]
3418 pub token: Option<String>,
3419 #[arg(
3420 long,
3421 help = "cloudflared log level: debug, info, warn, error, or fatal"
3422 )]
3423 pub log_level: Option<String>,
3424}
3425
3426#[derive(Args, Debug)]
3427pub struct CloudflareTunnelQuickStartCmd {
3428 #[arg(help = "Local URL to expose, for example http://localhost:8080")]
3429 pub url: String,
3430}
3431
3432#[derive(Args, Debug)]
3433pub struct CloudflareWorkflowsCmd {
3434 #[command(subcommand)]
3435 pub command: CloudflareWorkflowsSubCommand,
3436}
3437
3438#[derive(Subcommand, Debug)]
3439pub enum CloudflareWorkflowsSubCommand {
3440 #[command(external_subcommand)]
3441 External(Vec<String>),
3442}
3443
3444#[derive(Args, Debug)]
3445pub struct CloudflareEnvCmd {
3446 #[command(subcommand)]
3447 pub command: CloudflareEnvSubCommand,
3448}
3449
3450#[derive(Subcommand, Debug)]
3451pub enum CloudflareEnvSubCommand {
3452 #[command(external_subcommand)]
3453 External(Vec<String>),
3454}
3455
3456#[derive(Subcommand, Debug)]
3457pub enum CloudflareContainersSubCommand {
3458 #[command(about = "Run wrangler containers list")]
3459 List(CloudflareContainersListCmd),
3460 #[command(
3461 about = "Run wrangler containers info for the configured or supplied application id"
3462 )]
3463 Info(CloudflareContainersInfoCmd),
3464 #[command(
3465 about = "Run wrangler containers instances for the configured or supplied application id"
3466 )]
3467 Instances(CloudflareContainersInstancesCmd),
3468 #[command(about = "Run wrangler containers push")]
3469 Push(CloudflareContainersPushCmd),
3470 #[command(about = "Open wrangler containers ssh for an instance")]
3471 Ssh(CloudflareContainersSshCmd),
3472}
3473
3474#[derive(Args, Debug, Default)]
3475pub struct CloudflareContainersListCmd {
3476 #[arg(long, help = "Ask Wrangler for JSON output when supported")]
3477 pub json: bool,
3478}
3479
3480#[derive(Args, Debug, Default)]
3481pub struct CloudflareContainersInfoCmd {
3482 #[arg(long, help = "Container application id override")]
3483 pub application_id: Option<String>,
3484 #[arg(long, help = "Ask Wrangler for JSON output when supported")]
3485 pub json: bool,
3486}
3487
3488#[derive(Args, Debug, Default)]
3489pub struct CloudflareContainersInstancesCmd {
3490 #[arg(long, help = "Container application id override")]
3491 pub application_id: Option<String>,
3492 #[arg(long, help = "Ask Wrangler for JSON output when supported")]
3493 pub json: bool,
3494}
3495
3496#[derive(Args, Debug)]
3497pub struct CloudflareContainersPushCmd {
3498 #[arg(help = "Image tag or image name to push through Wrangler")]
3499 pub image: String,
3500}
3501
3502#[derive(Args, Debug)]
3503pub struct CloudflareContainersSshCmd {
3504 #[arg(help = "Container instance id")]
3505 pub instance_id: String,
3506 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
3507 pub args: Vec<String>,
3508}
3509
3510#[derive(Args, Debug)]
3511pub struct CloudflareJobsCmd {
3512 #[command(subcommand)]
3513 pub command: CloudflareJobsSubCommand,
3514}
3515
3516#[derive(Subcommand, Debug)]
3517pub enum CloudflareJobsSubCommand {
3518 #[command(about = "Queue a local Cloudflare workflow job")]
3519 Enqueue(CloudflareJobsEnqueueCmd),
3520 #[command(about = "Run queued local Cloudflare workflow jobs")]
3521 Run(CloudflareJobsRunCmd),
3522 #[command(about = "Show one local Cloudflare workflow job")]
3523 Status(CloudflareJobsStatusCmd),
3524 #[command(about = "List local Cloudflare workflow jobs")]
3525 List(CloudflareJobsListCmd),
3526 #[command(about = "Print stored logs for one local Cloudflare workflow job")]
3527 Logs(CloudflareJobsLogsCmd),
3528}
3529
3530#[derive(Args, Debug)]
3531pub struct CloudflareJobsEnqueueCmd {
3532 #[arg(value_enum)]
3533 pub workflow: CloudflareJobWorkflow,
3534 #[arg(long, value_enum, default_value_t = CloudflareRollout::Immediate)]
3535 pub rollout: CloudflareRollout,
3536 #[arg(long, help = "Required for release jobs")]
3537 pub version: Option<String>,
3538 #[arg(long, help = "Queue deploy jobs in dry-run mode")]
3539 pub dry_run: bool,
3540 #[arg(long, help = "Queue verification without invoking Wrangler deploy")]
3541 pub skip_deploy: bool,
3542 #[arg(long, help = "Allow an unchanged container image during verification")]
3543 pub allow_unchanged_container_image: bool,
3544 #[arg(long, help = "Prune old container image tags after verification")]
3545 pub prune_old_images: bool,
3546 #[arg(long, help = "Image tag count to retain when pruning old tags")]
3547 pub keep_image_tag_count: Option<usize>,
3548}
3549
3550#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ValueEnum)]
3551#[serde(rename_all = "kebab-case")]
3552pub enum CloudflareJobWorkflow {
3553 Doctor,
3554 Deploy,
3555 Release,
3556}
3557
3558impl CloudflareJobWorkflow {
3559 pub fn as_str(self) -> &'static str {
3560 match self {
3561 Self::Doctor => "doctor",
3562 Self::Deploy => "deploy",
3563 Self::Release => "release",
3564 }
3565 }
3566}
3567
3568#[derive(Args, Debug, Default)]
3569pub struct CloudflareJobsRunCmd {
3570 #[arg(long, help = "Run only the next queued job")]
3571 pub once: bool,
3572}
3573
3574#[derive(Args, Debug)]
3575pub struct CloudflareJobsStatusCmd {
3576 pub job_id: String,
3577}
3578
3579#[derive(Args, Debug, Default)]
3580pub struct CloudflareJobsListCmd {}
3581
3582#[derive(Args, Debug)]
3583pub struct CloudflareJobsLogsCmd {
3584 pub job_id: String,
3585}
3586
3587#[cfg(feature = "secrets")]
3588#[derive(Args, Debug)]
3589pub struct SecretsCmd {
3590 #[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
3591 pub provider: SecretsProviderKind,
3592 #[arg(long, help = "GitHub repository override (owner/repo)")]
3593 pub repo: Option<String>,
3594 #[arg(
3595 long,
3596 help = "Provider token override (GitHub token or Cloudflare API token)"
3597 )]
3598 pub token: Option<String>,
3599 #[arg(long, help = "Cloudflare account ID override")]
3600 pub account_id: Option<String>,
3601 #[arg(
3602 long = "environment",
3603 alias = "env",
3604 default_value = "xbp-dev",
3605 help = "Environment to sync (default: xbp-dev). Nested services are scoped automatically, e.g. xbp-dev-web."
3606 )]
3607 pub environment: String,
3608 #[arg(
3609 long,
3610 help = "Service name from .xbp/xbp.yaml. If omitted, XBP resolves it from the current directory or prompts when ambiguous."
3611 )]
3612 pub service: Option<String>,
3613 #[command(subcommand)]
3614 pub command: Option<SecretsSubCommand>,
3615}
3616
3617#[cfg(feature = "secrets")]
3618#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
3619pub enum SecretsProviderKind {
3620 Github,
3621 Cloudflare,
3622 Railway,
3623 Vercel,
3624}
3625
3626#[cfg(feature = "secrets")]
3627#[derive(Subcommand, Debug)]
3628pub enum SecretsSubCommand {
3629 #[command(alias = "ls", alias = "list-providers")]
3631 Providers,
3632 List(ListCmd),
3634 Push(PushCmd),
3636 Pull(PullCmd),
3638 GenerateDefault(GenerateDefaultCmd),
3640 GenerateExample(GenerateExampleCmd),
3642 Diff,
3644 Verify,
3646 #[command(name = "diag", alias = "doctor")]
3648 Diag,
3649 Stores(SecretsStoresCmd),
3651 Secrets(CloudflareSecretsCmd),
3653 Quota(SecretsQuotaCmd),
3655 #[command(name = "usage")]
3657 Usage,
3658}
3659
3660#[cfg(feature = "secrets")]
3661#[derive(Args, Debug)]
3662pub struct ListCmd {
3663 #[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
3664 pub file: Option<String>,
3665 #[arg(long, help = "Output format: plain (default) or json")]
3666 pub format: Option<String>,
3667}
3668
3669#[cfg(feature = "secrets")]
3670#[derive(Args, Debug)]
3671pub struct PushCmd {
3672 #[arg(long, help = "Path to env file (default: .env.local/.env)")]
3673 pub file: Option<String>,
3674 #[arg(
3675 long = "dev-vars-file",
3676 help = "Path to Cloudflare .dev.vars file. Defaults to .dev.vars when it exists."
3677 )]
3678 pub dev_vars_file: Option<String>,
3679 #[arg(
3680 long,
3681 help = "Skip the Cloudflare .dev.vars GitHub environment sync lane"
3682 )]
3683 pub skip_dev_vars: bool,
3684 #[arg(
3685 long,
3686 help = "Force overwrite existing GitHub Actions environment variables"
3687 )]
3688 pub force: bool,
3689 #[arg(long, help = "Show what would be pushed without making changes")]
3690 pub dry_run: bool,
3691}
3692
3693#[cfg(feature = "secrets")]
3694#[derive(Args, Debug)]
3695pub struct PullCmd {
3696 #[arg(long, help = "Output file path (default: .env.local)")]
3697 pub output: Option<String>,
3698 #[arg(
3699 long = "dev-vars-output",
3700 help = "Output path for Cloudflare .dev.vars. Defaults to .dev.vars when it already exists."
3701 )]
3702 pub dev_vars_output: Option<String>,
3703 #[arg(
3704 long,
3705 help = "Pull the Cloudflare .dev.vars environment even when .dev.vars does not exist yet"
3706 )]
3707 pub include_dev_vars: bool,
3708 #[arg(
3709 long,
3710 help = "Skip the Cloudflare .dev.vars GitHub environment sync lane"
3711 )]
3712 pub skip_dev_vars: bool,
3713}
3714
3715#[cfg(feature = "secrets")]
3716#[derive(Args, Debug)]
3717pub struct GenerateDefaultCmd {
3718 #[arg(long, help = "Output file path (default: .env.default)")]
3719 pub output: Option<String>,
3720}
3721
3722#[cfg(feature = "secrets")]
3723#[derive(Args, Debug)]
3724pub struct GenerateExampleCmd {
3725 #[arg(long, help = "Output file path (default: .env.example)")]
3726 pub output: Option<String>,
3727 #[arg(long, help = "Remove keys from .env.local not in .env.example")]
3728 pub clean: bool,
3729 #[arg(long, help = "Only include vars matching prefix (repeatable)")]
3730 pub include_prefix: Vec<String>,
3731 #[arg(long, help = "Exclude vars matching prefix (repeatable)")]
3732 pub exclude_prefix: Vec<String>,
3733}
3734
3735#[cfg(feature = "secrets")]
3736#[derive(Args, Debug)]
3737pub struct SecretsStoresCmd {
3738 #[command(subcommand)]
3739 pub command: SecretsStoresSubCommand,
3740}
3741
3742#[cfg(feature = "secrets")]
3743#[derive(Subcommand, Debug)]
3744pub enum SecretsStoresSubCommand {
3745 List(CloudflareSecretsStoreListCmd),
3746 Get(CloudflareSecretsStoreGetCmd),
3747 Create(CloudflareSecretsStoreCreateCmd),
3748 Delete(CloudflareSecretsStoreDeleteCmd),
3749}
3750
3751#[cfg(feature = "secrets")]
3752#[derive(Args, Debug)]
3753pub struct CloudflareSecretsStoreListCmd {}
3754
3755#[cfg(feature = "secrets")]
3756#[derive(Args, Debug)]
3757pub struct CloudflareSecretsStoreGetCmd {
3758 #[arg(long)]
3759 pub store_id: String,
3760}
3761
3762#[cfg(feature = "secrets")]
3763#[derive(Args, Debug)]
3764pub struct CloudflareSecretsStoreCreateCmd {
3765 #[arg(long)]
3766 pub name: String,
3767}
3768
3769#[cfg(feature = "secrets")]
3770#[derive(Args, Debug)]
3771pub struct CloudflareSecretsStoreDeleteCmd {
3772 #[arg(long)]
3773 pub store_id: String,
3774}
3775
3776#[cfg(feature = "secrets")]
3777#[derive(Args, Debug)]
3778pub struct CloudflareSecretsCmd {
3779 #[command(subcommand)]
3780 pub command: CloudflareSecretsSubCommand,
3781}
3782
3783#[cfg(feature = "secrets")]
3784#[derive(Subcommand, Debug)]
3785pub enum CloudflareSecretsSubCommand {
3786 List(CloudflareSecretsListCmd),
3787 Get(CloudflareSecretsGetCmd),
3788 Create(CloudflareSecretsCreateCmd),
3789 Edit(CloudflareSecretsEditCmd),
3790 Delete(CloudflareSecretsDeleteCmd),
3791 #[command(name = "delete-bulk")]
3792 DeleteBulk(CloudflareSecretsBulkDeleteCmd),
3793 Duplicate(CloudflareSecretsDuplicateCmd),
3794}
3795
3796#[cfg(feature = "secrets")]
3797#[derive(Args, Debug)]
3798pub struct CloudflareSecretsListCmd {
3799 #[arg(long)]
3800 pub store_id: String,
3801}
3802
3803#[cfg(feature = "secrets")]
3804#[derive(Args, Debug)]
3805pub struct CloudflareSecretsGetCmd {
3806 #[arg(long)]
3807 pub store_id: String,
3808 #[arg(long)]
3809 pub secret_id: String,
3810}
3811
3812#[cfg(feature = "secrets")]
3813#[derive(Args, Debug)]
3814pub struct CloudflareSecretsCreateCmd {
3815 #[arg(long)]
3816 pub store_id: String,
3817 #[arg(long)]
3818 pub name: String,
3819 #[arg(long)]
3820 pub value: String,
3821 #[arg(long, value_delimiter = ',')]
3822 pub scopes: Vec<String>,
3823 #[arg(long)]
3824 pub comment: Option<String>,
3825}
3826
3827#[cfg(feature = "secrets")]
3828#[derive(Args, Debug)]
3829pub struct CloudflareSecretsEditCmd {
3830 #[arg(long)]
3831 pub store_id: String,
3832 #[arg(long)]
3833 pub secret_id: String,
3834 #[arg(long)]
3835 pub name: Option<String>,
3836 #[arg(long)]
3837 pub value: Option<String>,
3838 #[arg(long, value_delimiter = ',')]
3839 pub scopes: Vec<String>,
3840 #[arg(long)]
3841 pub comment: Option<String>,
3842}
3843
3844#[cfg(feature = "secrets")]
3845#[derive(Args, Debug)]
3846pub struct CloudflareSecretsDeleteCmd {
3847 #[arg(long)]
3848 pub store_id: String,
3849 #[arg(long)]
3850 pub secret_id: String,
3851}
3852
3853#[cfg(feature = "secrets")]
3854#[derive(Args, Debug)]
3855pub struct CloudflareSecretsBulkDeleteCmd {
3856 #[arg(long)]
3857 pub store_id: String,
3858 #[arg(long = "secret-id", required = true)]
3859 pub secret_ids: Vec<String>,
3860}
3861
3862#[cfg(feature = "secrets")]
3863#[derive(Args, Debug)]
3864pub struct CloudflareSecretsDuplicateCmd {
3865 #[arg(long)]
3866 pub store_id: String,
3867 #[arg(long)]
3868 pub secret_id: String,
3869 #[arg(long)]
3870 pub name: String,
3871 #[arg(long, value_delimiter = ',')]
3872 pub scopes: Vec<String>,
3873 #[arg(long)]
3874 pub comment: Option<String>,
3875}
3876
3877#[cfg(feature = "secrets")]
3878#[derive(Args, Debug)]
3879pub struct SecretsQuotaCmd {
3880 #[command(subcommand)]
3881 pub command: SecretsQuotaSubCommand,
3882}
3883
3884#[cfg(feature = "secrets")]
3885#[derive(Subcommand, Debug)]
3886pub enum SecretsQuotaSubCommand {
3887 Get(SecretsQuotaGetCmd),
3888}
3889
3890#[cfg(feature = "secrets")]
3891#[derive(Args, Debug)]
3892pub struct SecretsQuotaGetCmd {}
3893
3894const DNS_HELP_TEMPLATE: &str = crate::cli::help_render::XBP_HELP_TEMPLATE;
3895
3896const DNS_COMMAND_AFTER_HELP: &str = "\
3897Examples:
3898 xbp dns providers
3899 xbp dns zones list --provider cloudflare --account-id acc_123
3900 xbp dns records list --provider cloudflare --zone-id zone_123
3901 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
3902 xbp dns dnssec get --provider cloudflare --zone-id zone_123
3903 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
3904
3905Notes:
3906 Start with `xbp dns providers` to see what is implemented today.
3907 Cloudflare auth comes from `--token`, `CLOUDFLARE_API_TOKEN`, `xbp config cloudflare set-key`, or a linked dashboard account after `xbp login` (`xbp config cloudflare login`).";
3908
3909const DNS_ZONES_AFTER_HELP: &str = "\
3910Examples:
3911 xbp dns zones list --provider cloudflare --account-id acc_123
3912 xbp dns zones get --provider cloudflare --zone-id zone_123
3913 xbp dns zones create --provider cloudflare --name example.com --account-id acc_123 --jump-start
3914 xbp dns zones edit --provider cloudflare --zone-id zone_123 --paused true
3915 xbp dns zones delete --provider cloudflare --zone-id zone_123";
3916
3917const DNS_RECORDS_AFTER_HELP: &str = "\
3918Examples:
3919 xbp dns records list --provider cloudflare --zone-id zone_123
3920 xbp dns records get --provider cloudflare --zone-id zone_123 --record-id rec_123
3921 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
3922 xbp dns records edit --provider cloudflare --zone-id zone_123 --record-id rec_123 --proxied true
3923 xbp dns records import --provider cloudflare --zone-id zone_123 --file zone.txt
3924 xbp dns records export --provider cloudflare --zone-id zone_123 --output zone.txt";
3925
3926const DNS_DNSSEC_AFTER_HELP: &str = "\
3927Examples:
3928 xbp dns dnssec get --provider cloudflare --zone-id zone_123
3929 xbp dns dnssec edit --provider cloudflare --zone-id zone_123 --status active";
3930
3931const DNS_SETTINGS_AFTER_HELP: &str = "\
3932Examples:
3933 xbp dns settings get --provider cloudflare --zone-id zone_123
3934 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
3935 xbp dns settings edit --provider cloudflare --zone-id zone_123 --nameservers-type custom --nameservers-ns-set 2";
3936
3937const DNS_PROVIDERS_AFTER_HELP: &str = "\
3938Examples:
3939 xbp dns providers
3940
3941What this shows:
3942 Implemented providers are wired into `xbp dns` today.
3943 Planned providers are tracked in the CLI surface but not callable yet.";
3944
3945#[derive(Args, Debug)]
3946#[command(
3947 about = "Manage DNS providers, zones, records, DNSSEC, and provider-level settings",
3948 arg_required_else_help = true,
3949 disable_help_subcommand = true,
3950 help_template = DNS_HELP_TEMPLATE,
3951 after_help = DNS_COMMAND_AFTER_HELP
3952)]
3953pub struct DnsCmd {
3954 #[command(subcommand)]
3955 pub command: DnsSubCommand,
3956}
3957
3958#[derive(Subcommand, Debug)]
3959pub enum DnsSubCommand {
3960 #[command(
3961 alias = "ls",
3962 alias = "list",
3963 about = "List supported DNS providers and current implementation status",
3964 after_help = DNS_PROVIDERS_AFTER_HELP
3965 )]
3966 Providers,
3967 #[command(about = "Inspect and manage DNS zones")]
3968 Zones(DnsZonesCmd),
3969 #[command(about = "List, create, edit, import, export, and batch DNS records")]
3970 Records(DnsRecordsCmd),
3971 #[command(about = "Inspect or edit DNSSEC status for a zone")]
3972 Dnssec(DnssecCmd),
3973 #[command(about = "Inspect or edit provider DNS settings for a zone")]
3974 Settings(DnsSettingsCmd),
3975}
3976
3977#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
3978pub enum DnsProviderKind {
3979 Cloudflare,
3980 Hetzner,
3981 Vercel,
3982 Custom,
3983}
3984
3985#[derive(Args, Debug)]
3986#[command(
3987 about = "Inspect and manage DNS zones",
3988 arg_required_else_help = true,
3989 help_template = DNS_HELP_TEMPLATE,
3990 after_help = DNS_ZONES_AFTER_HELP
3991)]
3992pub struct DnsZonesCmd {
3993 #[command(subcommand)]
3994 pub command: DnsZonesSubCommand,
3995}
3996
3997#[derive(Subcommand, Debug)]
3998pub enum DnsZonesSubCommand {
3999 #[command(about = "List zones for a provider account")]
4000 List(DnsZoneListCmd),
4001 #[command(about = "Fetch one zone by id")]
4002 Get(DnsZoneGetCmd),
4003 #[command(about = "Create a new zone")]
4004 Create(DnsZoneCreateCmd),
4005 #[command(about = "Edit zone-level properties")]
4006 Edit(DnsZoneEditCmd),
4007 #[command(about = "Delete a zone")]
4008 Delete(DnsZoneDeleteCmd),
4009}
4010
4011#[derive(Args, Debug)]
4012pub struct DnsZoneListCmd {
4013 #[arg(long, value_enum, default_value = "cloudflare")]
4014 pub provider: DnsProviderKind,
4015 #[arg(long)]
4016 pub account_id: Option<String>,
4017 #[arg(long)]
4018 pub account_name: Option<String>,
4019 #[arg(long = "account-name-op")]
4020 pub account_name_op: Option<String>,
4021 #[arg(long)]
4022 pub name: Option<String>,
4023 #[arg(long = "name-op")]
4024 pub name_op: Option<String>,
4025 #[arg(long)]
4026 pub status: Option<String>,
4027 #[arg(long = "type", value_delimiter = ',')]
4028 pub zone_types: Vec<String>,
4029 #[arg(long)]
4030 pub r#match: Option<String>,
4031 #[arg(long)]
4032 pub order: Option<String>,
4033 #[arg(long)]
4034 pub direction: Option<String>,
4035 #[arg(long)]
4036 pub page: Option<u64>,
4037 #[arg(long = "per-page")]
4038 pub per_page: Option<u64>,
4039 #[arg(long)]
4040 pub token: Option<String>,
4041}
4042
4043#[derive(Args, Debug)]
4044pub struct DnsZoneGetCmd {
4045 #[arg(long, value_enum, default_value = "cloudflare")]
4046 pub provider: DnsProviderKind,
4047 #[arg(long)]
4048 pub zone_id: String,
4049 #[arg(long)]
4050 pub token: Option<String>,
4051}
4052
4053#[derive(Args, Debug)]
4054pub struct DnsZoneCreateCmd {
4055 #[arg(long, value_enum, default_value = "cloudflare")]
4056 pub provider: DnsProviderKind,
4057 #[arg(long)]
4058 pub name: String,
4059 #[arg(long)]
4060 pub account_id: Option<String>,
4061 #[arg(long)]
4062 pub jump_start: bool,
4063 #[arg(long = "type")]
4064 pub zone_type: Option<String>,
4065 #[arg(long)]
4066 pub token: Option<String>,
4067}
4068
4069#[derive(Args, Debug)]
4070pub struct DnsZoneEditCmd {
4071 #[arg(long, value_enum, default_value = "cloudflare")]
4072 pub provider: DnsProviderKind,
4073 #[arg(long)]
4074 pub zone_id: String,
4075 #[arg(long)]
4076 pub paused: Option<bool>,
4077 #[arg(long = "type")]
4078 pub zone_type: Option<String>,
4079 #[arg(long = "vanity-name-server")]
4080 pub vanity_name_servers: Vec<String>,
4081 #[arg(long)]
4082 pub token: Option<String>,
4083}
4084
4085#[derive(Args, Debug)]
4086pub struct DnsZoneDeleteCmd {
4087 #[arg(long, value_enum, default_value = "cloudflare")]
4088 pub provider: DnsProviderKind,
4089 #[arg(long)]
4090 pub zone_id: String,
4091 #[arg(long)]
4092 pub token: Option<String>,
4093}
4094
4095#[derive(Args, Debug)]
4096#[command(
4097 about = "List, create, edit, import, export, and batch DNS records",
4098 arg_required_else_help = true,
4099 help_template = DNS_HELP_TEMPLATE,
4100 after_help = DNS_RECORDS_AFTER_HELP
4101)]
4102pub struct DnsRecordsCmd {
4103 #[command(subcommand)]
4104 pub command: DnsRecordsSubCommand,
4105}
4106
4107#[derive(Subcommand, Debug)]
4108pub enum DnsRecordsSubCommand {
4109 #[command(about = "List records in a zone")]
4110 List(DnsRecordListCmd),
4111 #[command(about = "Fetch one record by id")]
4112 Get(DnsRecordGetCmd),
4113 #[command(about = "Create a new DNS record")]
4114 Create(DnsRecordCreateCmd),
4115 #[command(about = "Replace a record by id with a full payload")]
4116 Replace(DnsRecordReplaceCmd),
4117 #[command(about = "Patch selected fields on a record")]
4118 Edit(DnsRecordEditCmd),
4119 #[command(about = "Delete a record")]
4120 Delete(DnsRecordDeleteCmd),
4121 #[command(about = "Apply a Cloudflare batch record payload from JSON")]
4122 Batch(DnsRecordBatchCmd),
4123 #[command(about = "Import a BIND-style zone file into a zone")]
4124 Import(DnsRecordImportCmd),
4125 #[command(about = "Export a zone as a BIND-style zone file")]
4126 Export(DnsRecordExportCmd),
4127}
4128
4129#[derive(Args, Debug)]
4130pub struct DnsRecordListCmd {
4131 #[arg(long, value_enum, default_value = "cloudflare")]
4132 pub provider: DnsProviderKind,
4133 #[arg(long)]
4134 pub zone_id: String,
4135 #[arg(long = "type")]
4136 pub record_type: Option<String>,
4137 #[arg(long)]
4138 pub name: Option<String>,
4139 #[arg(long)]
4140 pub page: Option<u64>,
4141 #[arg(long = "per-page")]
4142 pub per_page: Option<u64>,
4143 #[arg(long)]
4144 pub token: Option<String>,
4145}
4146
4147#[derive(Args, Debug)]
4148pub struct DnsRecordGetCmd {
4149 #[arg(long, value_enum, default_value = "cloudflare")]
4150 pub provider: DnsProviderKind,
4151 #[arg(long)]
4152 pub zone_id: String,
4153 #[arg(long)]
4154 pub record_id: String,
4155 #[arg(long)]
4156 pub token: Option<String>,
4157}
4158
4159#[derive(Args, Debug)]
4160pub struct DnsRecordCreateCmd {
4161 #[arg(long, value_enum, default_value = "cloudflare")]
4162 pub provider: DnsProviderKind,
4163 #[arg(long)]
4164 pub zone_id: String,
4165 #[arg(long = "type")]
4166 pub record_type: String,
4167 #[arg(long)]
4168 pub name: String,
4169 #[arg(long)]
4170 pub content: String,
4171 #[arg(long)]
4172 pub ttl: Option<u32>,
4173 #[arg(long)]
4174 pub proxied: Option<bool>,
4175 #[arg(long)]
4176 pub priority: Option<u32>,
4177 #[arg(long)]
4178 pub comment: Option<String>,
4179 #[arg(long = "tag")]
4180 pub tags: Vec<String>,
4181 #[arg(long = "data-json")]
4182 pub data_json: Option<String>,
4183 #[arg(long = "settings-json")]
4184 pub settings_json: Option<String>,
4185 #[arg(long)]
4186 pub token: Option<String>,
4187}
4188
4189#[derive(Args, Debug)]
4190pub struct DnsRecordReplaceCmd {
4191 #[command(flatten)]
4192 pub common: DnsRecordCreateCmd,
4193 #[arg(long)]
4194 pub record_id: String,
4195}
4196
4197#[derive(Args, Debug)]
4198pub struct DnsRecordEditCmd {
4199 #[arg(long, value_enum, default_value = "cloudflare")]
4200 pub provider: DnsProviderKind,
4201 #[arg(long)]
4202 pub zone_id: String,
4203 #[arg(long)]
4204 pub record_id: String,
4205 #[arg(long = "type")]
4206 pub record_type: Option<String>,
4207 #[arg(long)]
4208 pub name: Option<String>,
4209 #[arg(long)]
4210 pub content: Option<String>,
4211 #[arg(long)]
4212 pub ttl: Option<u32>,
4213 #[arg(long)]
4214 pub proxied: Option<bool>,
4215 #[arg(long)]
4216 pub priority: Option<u32>,
4217 #[arg(long)]
4218 pub comment: Option<String>,
4219 #[arg(long = "tag")]
4220 pub tags: Vec<String>,
4221 #[arg(long = "data-json")]
4222 pub data_json: Option<String>,
4223 #[arg(long = "settings-json")]
4224 pub settings_json: Option<String>,
4225 #[arg(long)]
4226 pub token: Option<String>,
4227}
4228
4229#[derive(Args, Debug)]
4230pub struct DnsRecordDeleteCmd {
4231 #[arg(long, value_enum, default_value = "cloudflare")]
4232 pub provider: DnsProviderKind,
4233 #[arg(long)]
4234 pub zone_id: String,
4235 #[arg(long)]
4236 pub record_id: String,
4237 #[arg(long)]
4238 pub token: Option<String>,
4239}
4240
4241#[derive(Args, Debug)]
4242pub struct DnsRecordBatchCmd {
4243 #[arg(long, value_enum, default_value = "cloudflare")]
4244 pub provider: DnsProviderKind,
4245 #[arg(long)]
4246 pub zone_id: String,
4247 #[arg(long)]
4248 pub input: PathBuf,
4249 #[arg(long)]
4250 pub token: Option<String>,
4251}
4252
4253#[derive(Args, Debug)]
4254pub struct DnsRecordImportCmd {
4255 #[arg(long, value_enum, default_value = "cloudflare")]
4256 pub provider: DnsProviderKind,
4257 #[arg(long)]
4258 pub zone_id: String,
4259 #[arg(long)]
4260 pub file: PathBuf,
4261 #[arg(long)]
4262 pub token: Option<String>,
4263}
4264
4265#[derive(Args, Debug)]
4266pub struct DnsRecordExportCmd {
4267 #[arg(long, value_enum, default_value = "cloudflare")]
4268 pub provider: DnsProviderKind,
4269 #[arg(long)]
4270 pub zone_id: String,
4271 #[arg(long)]
4272 pub output: Option<PathBuf>,
4273 #[arg(long)]
4274 pub token: Option<String>,
4275}
4276
4277#[derive(Args, Debug)]
4278#[command(
4279 about = "Inspect or edit DNSSEC status for a zone",
4280 arg_required_else_help = true,
4281 help_template = DNS_HELP_TEMPLATE,
4282 after_help = DNS_DNSSEC_AFTER_HELP
4283)]
4284pub struct DnssecCmd {
4285 #[command(subcommand)]
4286 pub command: DnssecSubCommand,
4287}
4288
4289#[derive(Subcommand, Debug)]
4290pub enum DnssecSubCommand {
4291 #[command(about = "Fetch DNSSEC state for a zone")]
4292 Get(DnssecGetCmd),
4293 #[command(about = "Edit DNSSEC-related flags for a zone")]
4294 Edit(DnssecEditCmd),
4295}
4296
4297#[derive(Args, Debug)]
4298pub struct DnssecGetCmd {
4299 #[arg(long, value_enum, default_value = "cloudflare")]
4300 pub provider: DnsProviderKind,
4301 #[arg(long)]
4302 pub zone_id: String,
4303 #[arg(long)]
4304 pub token: Option<String>,
4305}
4306
4307#[derive(Args, Debug)]
4308pub struct DnssecEditCmd {
4309 #[arg(long, value_enum, default_value = "cloudflare")]
4310 pub provider: DnsProviderKind,
4311 #[arg(long)]
4312 pub zone_id: String,
4313 #[arg(long)]
4314 pub status: Option<String>,
4315 #[arg(long = "dnssec-multi-signer")]
4316 pub dnssec_multi_signer: Option<bool>,
4317 #[arg(long = "dnssec-presigned")]
4318 pub dnssec_presigned: Option<bool>,
4319 #[arg(long = "dnssec-use-nsec3")]
4320 pub dnssec_use_nsec3: Option<bool>,
4321 #[arg(long)]
4322 pub token: Option<String>,
4323}
4324
4325#[derive(Args, Debug)]
4326#[command(
4327 about = "Inspect or edit provider DNS settings for a zone",
4328 arg_required_else_help = true,
4329 help_template = DNS_HELP_TEMPLATE,
4330 after_help = DNS_SETTINGS_AFTER_HELP
4331)]
4332pub struct DnsSettingsCmd {
4333 #[command(subcommand)]
4334 pub command: DnsSettingsSubCommand,
4335}
4336
4337#[derive(Subcommand, Debug)]
4338pub enum DnsSettingsSubCommand {
4339 #[command(about = "Fetch provider DNS settings for a zone")]
4340 Get(DnsSettingsGetCmd),
4341 #[command(about = "Edit provider DNS settings for a zone")]
4342 Edit(DnsSettingsEditCmd),
4343}
4344
4345#[derive(Args, Debug)]
4346pub struct DnsSettingsGetCmd {
4347 #[arg(long, value_enum, default_value = "cloudflare")]
4348 pub provider: DnsProviderKind,
4349 #[arg(long)]
4350 pub zone_id: String,
4351 #[arg(long)]
4352 pub token: Option<String>,
4353}
4354
4355#[derive(Args, Debug)]
4356pub struct DnsSettingsEditCmd {
4357 #[arg(long, value_enum, default_value = "cloudflare")]
4358 pub provider: DnsProviderKind,
4359 #[arg(long)]
4360 pub zone_id: String,
4361 #[arg(long)]
4362 pub flatten_all_cnames: Option<bool>,
4363 #[arg(long)]
4364 pub foundation_dns: Option<bool>,
4365 #[arg(long)]
4366 pub multi_provider: Option<bool>,
4367 #[arg(long)]
4368 pub ns_ttl: Option<u32>,
4369 #[arg(long)]
4370 pub secondary_overrides: Option<bool>,
4371 #[arg(long)]
4372 pub zone_mode: Option<String>,
4373 #[arg(long = "reference-zone-id")]
4374 pub reference_zone_id: Option<String>,
4375 #[arg(long = "nameservers-type")]
4376 pub nameservers_type: Option<String>,
4377 #[arg(long = "nameservers-ns-set")]
4378 pub nameservers_ns_set: Option<u32>,
4379 #[arg(long = "soa-json")]
4380 pub soa_json: Option<String>,
4381 #[arg(long)]
4382 pub token: Option<String>,
4383}
4384
4385#[derive(Args, Debug)]
4386#[command(
4387 arg_required_else_help = true,
4388 disable_help_subcommand = true,
4389 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
4390 after_help = crate::cli::help_render::DOMAINS_AFTER_HELP
4391)]
4392pub struct DomainsCmd {
4393 #[arg(long, value_enum, default_value = "cloudflare")]
4394 pub provider: DomainsProviderKind,
4395 #[arg(long)]
4396 pub account_id: Option<String>,
4397 #[arg(long)]
4398 pub token: Option<String>,
4399 #[command(subcommand)]
4400 pub command: DomainsSubCommand,
4401}
4402
4403#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
4404pub enum DomainsProviderKind {
4405 Cloudflare,
4406}
4407
4408#[derive(Subcommand, Debug)]
4409pub enum DomainsSubCommand {
4410 Search(DomainsSearchCmd),
4411 Check(DomainsCheckCmd),
4412 List(DomainsListCmd),
4413}
4414
4415#[derive(Args, Debug)]
4416pub struct DomainsSearchCmd {
4417 #[arg(long)]
4418 pub query: String,
4419 #[arg(long = "extension")]
4420 pub extensions: Vec<String>,
4421 #[arg(long)]
4422 pub limit: Option<usize>,
4423}
4424
4425#[derive(Args, Debug)]
4426pub struct DomainsCheckCmd {
4427 #[arg(long = "domain", required = true)]
4428 pub domains: Vec<String>,
4429}
4430
4431#[derive(Args, Debug)]
4432pub struct DomainsListCmd {}
4433
4434#[derive(Args, Debug)]
4435pub struct GenerateSystemdCmd {
4436 #[arg(
4437 long,
4438 default_value = "/etc/systemd/system",
4439 help = "Directory where the systemd units are written"
4440 )]
4441 pub output_dir: PathBuf,
4442 #[arg(long, help = "Only generate the unit for this service name")]
4443 pub service: Option<String>,
4444 #[arg(
4445 long,
4446 default_value_t = true,
4447 help = "Also generate the xbp-api systemd unit alongside project/services"
4448 )]
4449 pub api: bool,
4450}
4451
4452#[derive(Args, Debug)]
4453#[command(
4454 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
4455 after_help = crate::cli::help_render::DONE_AFTER_HELP
4456)]
4457pub struct DoneCmd {
4458 #[arg(long, help = "Root directory under which to discover git repos")]
4459 pub root: Option<std::path::PathBuf>,
4460 #[arg(
4461 long,
4462 default_value = "24 hours ago",
4463 help = "Git --since value (e.g. '7 days ago')"
4464 )]
4465 pub since: String,
4466 #[arg(short, long, help = "Output Markdown file path")]
4467 pub output: Option<std::path::PathBuf>,
4468 #[arg(long, help = "Skip AI summarization (OpenRouter)")]
4469 pub no_ai: bool,
4470 #[arg(short, long, help = "Discover repos recursively")]
4471 pub recursive: bool,
4472 #[arg(long, help = "Exclude repo by name (repeatable)")]
4473 pub exclude: Vec<String>,
4474}
4475
4476#[derive(Args, Debug)]
4477pub struct FixProcessMonitorJsonCmd {
4478 #[arg(help = "Path to a Cursor process-monitor JSON export")]
4479 pub path: std::path::PathBuf,
4480 #[arg(
4481 long,
4482 help = "Check whether the file needs repair without writing changes"
4483 )]
4484 pub check: bool,
4485 #[arg(
4486 long,
4487 help = "Print repaired JSON to stdout instead of overwriting the file"
4488 )]
4489 pub stdout: bool,
4490}
4491
4492#[derive(Args, Debug)]
4493pub struct CursorCmd {
4494 #[command(subcommand)]
4495 pub command: CursorSubCommand,
4496}
4497
4498#[derive(Subcommand, Debug)]
4499pub enum CursorSubCommand {
4500 #[command(about = "Upload local Cursor file history to the XBP dashboard")]
4501 Ingest {
4502 #[arg(
4503 long,
4504 help = "Scan local Cursor history without uploading to the dashboard"
4505 )]
4506 dry_run: bool,
4507 },
4508}
4509
4510#[cfg(feature = "nordvpn")]
4511#[derive(Args, Debug)]
4512pub struct NordvpnCmd {
4513 #[arg(
4514 trailing_var_arg = true,
4515 allow_hyphen_values = true,
4516 help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
4517 )]
4518 pub args: Vec<String>,
4519}
4520
4521#[cfg(feature = "kubernetes")]
4522#[derive(Args, Debug)]
4523pub struct KubernetesCmd {
4524 #[command(subcommand)]
4525 pub command: KubernetesSubCommand,
4526}
4527
4528#[cfg(feature = "kubernetes")]
4529#[derive(Args, Debug)]
4530pub struct KubernetesAddonCmd {
4531 #[command(subcommand)]
4532 pub command: KubernetesAddonSubCommand,
4533}
4534
4535#[cfg(feature = "kubernetes")]
4536#[derive(Subcommand, Debug)]
4537pub enum KubernetesAddonSubCommand {
4538 List,
4540 Enable {
4542 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
4543 name: String,
4544 },
4545 Disable {
4547 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
4548 name: String,
4549 },
4550}
4551
4552#[cfg(feature = "kubernetes")]
4553#[derive(Subcommand, Debug)]
4554pub enum KubernetesSubCommand {
4555 Check {
4557 #[arg(long, help = "Kubeconfig context to target")]
4558 context: Option<String>,
4559 #[arg(
4560 long,
4561 default_value = "default",
4562 help = "Namespace to probe for workload readiness"
4563 )]
4564 namespace: String,
4565 #[arg(long, help = "Skip live cluster calls (tooling check only)")]
4566 offline: bool,
4567 },
4568 Generate {
4570 #[arg(long, help = "Logical app name (used for resource names)")]
4571 name: String,
4572 #[arg(long, help = "Container image reference")]
4573 image: String,
4574 #[arg(long, default_value_t = 80, help = "Container port for the service")]
4575 port: u16,
4576 #[arg(long, default_value_t = 1, help = "Replica count")]
4577 replicas: u16,
4578 #[arg(
4579 long,
4580 default_value = "default",
4581 help = "Namespace for generated resources"
4582 )]
4583 namespace: String,
4584 #[arg(
4585 long,
4586 default_value = "k8s/xbp-manifest.yaml",
4587 help = "Path to write the manifest bundle"
4588 )]
4589 output: String,
4590 #[arg(long, help = "Optional ingress host (creates Ingress when set)")]
4591 host: Option<String>,
4592 },
4593 Apply {
4595 #[arg(long, help = "Path to manifest file")]
4596 file: String,
4597 #[arg(long, help = "Override kube context")]
4598 context: Option<String>,
4599 #[arg(long, help = "Override namespace")]
4600 namespace: Option<String>,
4601 #[arg(long, help = "Use --dry-run=server")]
4602 dry_run: bool,
4603 },
4604 Status {
4606 #[arg(long, default_value = "default", help = "Namespace to summarize")]
4607 namespace: String,
4608 #[arg(long, help = "Override kube context")]
4609 context: Option<String>,
4610 },
4611 Addons(KubernetesAddonCmd),
4613 DashboardToken {
4615 #[arg(
4616 long,
4617 default_value = "kube-system",
4618 help = "Namespace containing the dashboard token secret"
4619 )]
4620 namespace: String,
4621 #[arg(
4622 long,
4623 default_value = "microk8s-dashboard-token",
4624 help = "Secret name containing the dashboard login token"
4625 )]
4626 secret: String,
4627 #[arg(long, help = "Override kube context")]
4628 context: Option<String>,
4629 },
4630 ObservabilityCreds {
4632 #[arg(
4633 long,
4634 default_value = "observability",
4635 help = "Namespace containing Grafana secret"
4636 )]
4637 namespace: String,
4638 #[arg(
4639 long,
4640 default_value = "kube-prom-stack-grafana",
4641 help = "Grafana secret name"
4642 )]
4643 secret: String,
4644 #[arg(long, help = "Override kube context")]
4645 context: Option<String>,
4646 },
4647 Issuer {
4649 #[arg(
4650 long,
4651 help = "Email used for Let's Encrypt account registration (required)"
4652 )]
4653 email: String,
4654 #[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
4655 name: String,
4656 #[arg(
4657 long,
4658 default_value = "default",
4659 help = "Namespace for the Issuer resource"
4660 )]
4661 namespace: String,
4662 #[arg(
4663 long,
4664 default_value = "https://acme-v02.api.letsencrypt.org/directory",
4665 help = "ACME server URL (production by default)"
4666 )]
4667 server: String,
4668 #[arg(
4669 long,
4670 default_value = "letsencrypt-account-key",
4671 help = "Secret used to store the ACME account private key"
4672 )]
4673 private_key_secret: String,
4674 #[arg(
4675 long,
4676 default_value = "nginx",
4677 help = "Ingress class name used for HTTP01 solving"
4678 )]
4679 ingress_class_name: String,
4680 #[arg(long, help = "Override kube context")]
4681 context: Option<String>,
4682 #[arg(long, help = "Use --dry-run=server")]
4683 dry_run: bool,
4684 },
4685}
4686
4687#[cfg(test)]
4688mod tests {
4689 use super::{
4690 Cli, CloudflareConfigAction, CloudflareJobWorkflow, CloudflareRollout,
4691 CloudflareSubCommand, CloudflaredSubCommand, Commands, DnsProviderKind, DnsSubCommand,
4692 DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand, GenerateSubCommand,
4693 LinearConfigAction, NetworkFloatingIpSubCommand, NetworkHetznerSubCommand,
4694 NetworkHetznerVswitchSubCommand, NetworkSubCommand, SshCmd, WorktreeWatchSubCommand,
4695 };
4696 #[cfg(feature = "secrets")]
4697 use super::{
4698 CloudflareSecretsSubCommand, SecretsProviderKind, SecretsStoresSubCommand,
4699 SecretsSubCommand,
4700 };
4701 use clap::Parser;
4702 use std::path::PathBuf;
4703
4704 #[test]
4705 fn parses_network_floating_ip_add() {
4706 let cli = Cli::parse_from([
4707 "xbp",
4708 "network",
4709 "floating-ip",
4710 "add",
4711 "--ip",
4712 "1.2.3.4",
4713 "--apply",
4714 ]);
4715
4716 match cli.command {
4717 Some(Commands::Network(network)) => match network.command {
4718 NetworkSubCommand::FloatingIp(fip) => match fip.command {
4719 NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
4720 assert_eq!(ip, "1.2.3.4");
4721 assert!(apply);
4722 }
4723 _ => panic!("expected add subcommand"),
4724 },
4725 _ => panic!("expected floating-ip subcommand"),
4726 },
4727 _ => panic!("expected network command"),
4728 }
4729 }
4730
4731 #[test]
4732 fn parses_generate_config_update() {
4733 let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
4734
4735 match cli.command {
4736 Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
4737 GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
4738 _ => panic!("expected generate config command"),
4739 },
4740 _ => panic!("expected generate command"),
4741 }
4742 }
4743
4744 #[test]
4745 fn parses_commit_command_with_dry_run() {
4746 let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
4747
4748 match cli.command {
4749 Some(Commands::Commit(commit_cmd)) => {
4750 assert!(commit_cmd.dry_run);
4751 assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
4752 assert_eq!(commit_cmd.model, None);
4753 }
4754 _ => panic!("expected commit command"),
4755 }
4756 }
4757
4758 #[test]
4759 fn parses_linear_select_initiative_config_command() {
4760 let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
4761
4762 match cli.command {
4763 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
4764 Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
4765 assert!(matches!(
4766 linear_cmd.action,
4767 LinearConfigAction::SelectInitiative
4768 ));
4769 }
4770 _ => panic!("expected linear config provider"),
4771 },
4772 _ => panic!("expected config command"),
4773 }
4774 }
4775
4776 #[test]
4777 fn parses_ssh_command_with_cloudflared_and_key_auth() {
4778 let cli = Cli::parse_from([
4779 "xbp",
4780 "ssh",
4781 "--host",
4782 "ssh.internal",
4783 "--username",
4784 "deploy",
4785 "--private-key",
4786 "C:/Users/floris/.ssh/id_ed25519",
4787 "--cloudflared-hostname",
4788 "bastion.example.com",
4789 "--command",
4790 "htop",
4791 ]);
4792
4793 let Some(Commands::Ssh(SshCmd {
4794 ssh_host,
4795 ssh_username,
4796 private_key,
4797 cloudflared_hostname,
4798 command,
4799 ..
4800 })) = cli.command
4801 else {
4802 panic!("expected shell command");
4803 };
4804
4805 assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
4806 assert_eq!(ssh_username.as_deref(), Some("deploy"));
4807 assert_eq!(
4808 private_key,
4809 Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
4810 );
4811 assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
4812 assert_eq!(command.as_deref(), Some("htop"));
4813 }
4814
4815 #[test]
4816 fn parses_cloudflared_tcp_command() {
4817 let cli = Cli::parse_from([
4818 "xbp",
4819 "cloudflared",
4820 "tcp",
4821 "--hostname",
4822 "bastion.example.com",
4823 "--listener",
4824 "127.0.0.1:2222",
4825 ]);
4826
4827 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
4828 panic!("expected cloudflared command");
4829 };
4830
4831 match cloudflared_cmd.command {
4832 CloudflaredSubCommand::Tcp(tcp_cmd) => {
4833 assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
4834 assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
4835 }
4836 }
4837 }
4838
4839 #[test]
4840 fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
4841 let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
4842
4843 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
4844 panic!("expected cloudflared command");
4845 };
4846
4847 match cloudflared_cmd.command {
4848 CloudflaredSubCommand::Tcp(tcp_cmd) => {
4849 assert_eq!(tcp_cmd.hostname, None);
4850 assert_eq!(tcp_cmd.listener, None);
4851 }
4852 }
4853 }
4854
4855 #[test]
4856 fn parses_cloudflare_doctor_with_app_after_subcommand() {
4857 let cli = Cli::parse_from(["xbp", "cloudflare", "doctor", "--app", "auth"]);
4858
4859 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
4860 panic!("expected cloudflare command");
4861 };
4862 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
4863 assert!(matches!(
4864 cloudflare_cmd.command,
4865 CloudflareSubCommand::Doctor(_)
4866 ));
4867 }
4868
4869 #[test]
4870 fn parses_cloudflare_tunnel_run_with_token_and_log_level() {
4871 let cli = Cli::parse_from([
4872 "xbp",
4873 "cloudflare",
4874 "tunnel",
4875 "run",
4876 "--token",
4877 "tunnel-token",
4878 "--log-level",
4879 "debug",
4880 ]);
4881
4882 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
4883 panic!("expected cloudflare command");
4884 };
4885
4886 match cloudflare_cmd.command {
4887 CloudflareSubCommand::Tunnel(tunnel_cmd) => match tunnel_cmd.command {
4888 super::CloudflareTunnelSubCommand::Run(run_cmd) => {
4889 assert_eq!(run_cmd.tunnel, None);
4890 assert_eq!(run_cmd.token.as_deref(), Some("tunnel-token"));
4891 assert_eq!(run_cmd.log_level.as_deref(), Some("debug"));
4892 }
4893 _ => panic!("expected tunnel run command"),
4894 },
4895 _ => panic!("expected cloudflare tunnel command"),
4896 }
4897 }
4898
4899 #[test]
4900 fn parses_cloudflare_workflows_instance_command_with_local_flags() {
4901 let cli = Cli::parse_from([
4902 "xbp",
4903 "cloudflare",
4904 "workflows",
4905 "instances",
4906 "send-event",
4907 "my-workflow",
4908 "latest",
4909 "--type",
4910 "my-event",
4911 "--payload",
4912 "{\"key\":\"value\"}",
4913 "--local",
4914 "--port",
4915 "8787",
4916 ]);
4917
4918 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
4919 panic!("expected cloudflare command");
4920 };
4921
4922 match cloudflare_cmd.command {
4923 CloudflareSubCommand::Workflows(workflows_cmd) => match workflows_cmd.command {
4924 super::CloudflareWorkflowsSubCommand::External(args) => assert_eq!(
4925 args,
4926 [
4927 "instances",
4928 "send-event",
4929 "my-workflow",
4930 "latest",
4931 "--type",
4932 "my-event",
4933 "--payload",
4934 "{\"key\":\"value\"}",
4935 "--local",
4936 "--port",
4937 "8787"
4938 ]
4939 ),
4940 },
4941 _ => panic!("expected cloudflare workflows command"),
4942 }
4943 }
4944
4945 #[test]
4946 fn parses_cloudflare_env_dev_with_environment_and_port() {
4947 let cli = Cli::parse_from([
4948 "xbp",
4949 "cloudflare",
4950 "env",
4951 "dev",
4952 "--env",
4953 "staging",
4954 "--port",
4955 "8787",
4956 ]);
4957
4958 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
4959 panic!("expected cloudflare command");
4960 };
4961
4962 match cloudflare_cmd.command {
4963 CloudflareSubCommand::Env(env_cmd) => match env_cmd.command {
4964 super::CloudflareEnvSubCommand::External(args) => {
4965 assert_eq!(args, ["dev", "--env", "staging", "--port", "8787"])
4966 }
4967 },
4968 _ => panic!("expected cloudflare env command"),
4969 }
4970 }
4971
4972 #[test]
4973 fn parses_cloudflare_release_with_domain_guard() {
4974 let cli = Cli::parse_from([
4975 "xbp",
4976 "cloudflare",
4977 "release",
4978 "--app",
4979 "auth",
4980 "--version",
4981 "1.2.3",
4982 "--rollout",
4983 "gradual",
4984 "--domain",
4985 "auth-runtime",
4986 ]);
4987
4988 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
4989 panic!("expected cloudflare command");
4990 };
4991 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
4992 match cloudflare_cmd.command {
4993 CloudflareSubCommand::Release(release_cmd) => {
4994 assert_eq!(release_cmd.version, "1.2.3");
4995 assert_eq!(release_cmd.rollout, CloudflareRollout::Gradual);
4996 assert_eq!(release_cmd.domain.as_deref(), Some("auth-runtime"));
4997 }
4998 _ => panic!("expected release subcommand"),
4999 }
5000 }
5001
5002 #[test]
5003 fn parses_cloudflare_deploy_parity_flags() {
5004 let cli = Cli::parse_from([
5005 "xbp",
5006 "cloudflare",
5007 "deploy",
5008 "--app",
5009 "auth",
5010 "--rollout",
5011 "none",
5012 "--skip-deploy",
5013 "--allow-unchanged-container-image",
5014 "--prune-old-images",
5015 "--keep-image-tag-count",
5016 "7",
5017 ]);
5018
5019 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
5020 panic!("expected cloudflare command");
5021 };
5022 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
5023 match cloudflare_cmd.command {
5024 CloudflareSubCommand::Deploy(deploy_cmd) => {
5025 assert_eq!(deploy_cmd.rollout, CloudflareRollout::None);
5026 assert!(deploy_cmd.skip_deploy);
5027 assert!(deploy_cmd.allow_unchanged_container_image);
5028 assert!(deploy_cmd.prune_old_images);
5029 assert_eq!(deploy_cmd.keep_image_tag_count, Some(7));
5030 }
5031 _ => panic!("expected deploy subcommand"),
5032 }
5033 }
5034
5035 #[test]
5036 fn parses_cloudflare_release_parity_flags() {
5037 let cli = Cli::parse_from([
5038 "xbp",
5039 "cloudflare",
5040 "release",
5041 "--app",
5042 "auth",
5043 "--version",
5044 "1.2.3",
5045 "--rollout",
5046 "none",
5047 "--skip-deploy",
5048 "--allow-unchanged-container-image",
5049 "--prune-old-images",
5050 "--keep-image-tag-count",
5051 "5",
5052 ]);
5053
5054 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
5055 panic!("expected cloudflare command");
5056 };
5057 match cloudflare_cmd.command {
5058 CloudflareSubCommand::Release(release_cmd) => {
5059 assert_eq!(release_cmd.rollout, CloudflareRollout::None);
5060 assert!(release_cmd.skip_deploy);
5061 assert!(release_cmd.allow_unchanged_container_image);
5062 assert!(release_cmd.prune_old_images);
5063 assert_eq!(release_cmd.keep_image_tag_count, Some(5));
5064 }
5065 _ => panic!("expected release subcommand"),
5066 }
5067 }
5068
5069 #[test]
5070 fn parses_cloudflare_jobs_enqueue_with_workflow_payload() {
5071 let cli = Cli::parse_from([
5072 "xbp",
5073 "cloudflare",
5074 "jobs",
5075 "enqueue",
5076 "release",
5077 "--app",
5078 "auth",
5079 "--version",
5080 "1.2.3",
5081 "--rollout",
5082 "gradual",
5083 "--skip-deploy",
5084 "--prune-old-images",
5085 ]);
5086
5087 let Some(Commands::Cloudflare(cloudflare_cmd)) = cli.command else {
5088 panic!("expected cloudflare command");
5089 };
5090 assert_eq!(cloudflare_cmd.app.as_deref(), Some("auth"));
5091 match cloudflare_cmd.command {
5092 CloudflareSubCommand::Jobs(jobs_cmd) => match jobs_cmd.command {
5093 super::CloudflareJobsSubCommand::Enqueue(enqueue_cmd) => {
5094 assert_eq!(enqueue_cmd.workflow, CloudflareJobWorkflow::Release);
5095 assert_eq!(enqueue_cmd.version.as_deref(), Some("1.2.3"));
5096 assert_eq!(enqueue_cmd.rollout, CloudflareRollout::Gradual);
5097 assert!(enqueue_cmd.skip_deploy);
5098 assert!(enqueue_cmd.prune_old_images);
5099 }
5100 _ => panic!("expected enqueue subcommand"),
5101 },
5102 _ => panic!("expected jobs subcommand"),
5103 }
5104 }
5105
5106 #[test]
5107 fn parses_worktree_watch_stop_force_command() {
5108 let cli = Cli::parse_from([
5109 "xbp",
5110 "worktree-watch",
5111 "stop",
5112 "--repo",
5113 "C:/Users/floris/Documents/GitHub/xbp",
5114 "--force",
5115 ]);
5116
5117 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
5118 panic!("expected worktree-watch command");
5119 };
5120
5121 match worktree_cmd.command {
5122 WorktreeWatchSubCommand::Stop(stop_cmd) => {
5123 assert_eq!(
5124 stop_cmd.target.repo,
5125 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/xbp"))
5126 );
5127 assert!(stop_cmd.force);
5128 }
5129 _ => panic!("expected stop subcommand"),
5130 }
5131 }
5132
5133 #[test]
5134 fn parses_worktree_watch_parent_detach_command() {
5135 let cli = Cli::parse_from([
5136 "xbp",
5137 "worktree-watch",
5138 "start",
5139 "--parent",
5140 "C:/Users/floris/Documents/GitHub",
5141 "--detach",
5142 ]);
5143
5144 let Some(Commands::WorktreeWatch(worktree_cmd)) = cli.command else {
5145 panic!("expected worktree-watch command");
5146 };
5147
5148 match worktree_cmd.command {
5149 WorktreeWatchSubCommand::Start(start_cmd) => {
5150 assert_eq!(
5151 start_cmd.target.parent,
5152 Some(PathBuf::from("C:/Users/floris/Documents/GitHub"))
5153 );
5154 assert!(start_cmd.detach);
5155 }
5156 _ => panic!("expected start subcommand"),
5157 }
5158 }
5159
5160 #[test]
5161 fn parses_version_workspace_publish_run_command() {
5162 let cli = Cli::parse_from([
5163 "xbp",
5164 "version",
5165 "workspace",
5166 "publish",
5167 "run",
5168 "--repo",
5169 "C:/Users/floris/Documents/GitHub/athena",
5170 "--dry-run",
5171 "--from",
5172 "athena-s3",
5173 ]);
5174
5175 let Some(Commands::Version(version_cmd)) = cli.command else {
5176 panic!("expected version command");
5177 };
5178
5179 match version_cmd.command {
5180 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
5181 match workspace_cmd.command {
5182 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
5183 match publish_cmd.command {
5184 super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
5185 assert_eq!(
5186 run_cmd.target.repo,
5187 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
5188 );
5189 assert!(!run_cmd.target.json);
5190 assert!(run_cmd.dry_run);
5191 assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
5192 }
5193 _ => panic!("expected publish run"),
5194 }
5195 }
5196 _ => panic!("expected workspace publish"),
5197 }
5198 }
5199 _ => panic!("expected version workspace command"),
5200 }
5201 }
5202
5203 #[test]
5204 fn parses_version_workspace_publish_plan_with_only_and_include_prereqs() {
5205 let cli = Cli::parse_from([
5206 "xbp",
5207 "version",
5208 "workspace",
5209 "publish",
5210 "plan",
5211 "--repo",
5212 "C:/Users/floris/Documents/GitHub/athena-auth",
5213 "--only",
5214 "athena-auth",
5215 "--include-prereqs",
5216 ]);
5217
5218 let Some(Commands::Version(version_cmd)) = cli.command else {
5219 panic!("expected version command");
5220 };
5221
5222 match version_cmd.command {
5223 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
5224 match workspace_cmd.command {
5225 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
5226 match publish_cmd.command {
5227 super::VersionWorkspacePublishSubCommand::Plan(plan_cmd) => {
5228 assert_eq!(
5229 plan_cmd.target.repo,
5230 Some(PathBuf::from(
5231 "C:/Users/floris/Documents/GitHub/athena-auth"
5232 ))
5233 );
5234 assert_eq!(plan_cmd.only.as_deref(), Some("athena-auth"));
5235 assert!(plan_cmd.include_prereqs);
5236 }
5237 _ => panic!("expected publish plan"),
5238 }
5239 }
5240 _ => panic!("expected workspace publish"),
5241 }
5242 }
5243 _ => panic!("expected version workspace command"),
5244 }
5245 }
5246
5247 #[test]
5248 fn parses_commit_alias_with_push_flag() {
5249 let cli = Cli::parse_from(["xbp", "c", "-p"]);
5250
5251 let Some(Commands::Commit(commit_cmd)) = cli.command else {
5252 panic!("expected commit command");
5253 };
5254
5255 assert!(commit_cmd.push);
5256 assert!(!commit_cmd.dry_run);
5257 }
5258
5259 #[test]
5260 fn parses_version_alias_release_alias() {
5261 let cli = Cli::parse_from([
5262 "xbp",
5263 "v",
5264 "r",
5265 "--draft",
5266 "--publish",
5267 "--force",
5268 "--flag",
5269 "nightly",
5270 ]);
5271
5272 let Some(Commands::Version(version_cmd)) = cli.command else {
5273 panic!("expected version command");
5274 };
5275
5276 let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
5277 panic!("expected release subcommand");
5278 };
5279
5280 assert!(release_cmd.draft);
5281 assert!(release_cmd.publish);
5282 assert!(release_cmd.force);
5283 assert_eq!(release_cmd.flag, Some(super::VersionReleaseFlag::Nightly));
5284 }
5285
5286 #[test]
5287 fn parses_version_domain_release_guard_flags() {
5288 let cli = Cli::parse_from([
5289 "xbp",
5290 "version",
5291 "domain",
5292 "release",
5293 "--domain",
5294 "auth-runtime",
5295 "--version",
5296 "2.0.0",
5297 "--deploy",
5298 "--allow-major-jump",
5299 "--allow-cross-domain-version",
5300 "platform",
5301 ]);
5302
5303 let Some(Commands::Version(version_cmd)) = cli.command else {
5304 panic!("expected version command");
5305 };
5306
5307 let Some(super::VersionSubCommand::Domain(domain_cmd)) = version_cmd.command else {
5308 panic!("expected domain command");
5309 };
5310
5311 match domain_cmd.command {
5312 super::VersionDomainSubCommand::Release(release_cmd) => {
5313 assert_eq!(release_cmd.domain, "auth-runtime");
5314 assert_eq!(release_cmd.version.as_deref(), Some("2.0.0"));
5315 assert!(release_cmd.deploy);
5316 assert!(release_cmd.allow_major_jump);
5317 assert_eq!(
5318 release_cmd.allow_cross_domain_version.as_deref(),
5319 Some("platform")
5320 );
5321 }
5322 _ => panic!("expected domain release"),
5323 }
5324 }
5325
5326 #[test]
5327 fn parses_publish_command_target_filter() {
5328 let cli = Cli::parse_from([
5329 "xbp",
5330 "publish",
5331 "--allow-dirty",
5332 "--force",
5333 "--include-prereqs",
5334 "--target",
5335 "npm",
5336 "--service",
5337 "web",
5338 "--manifest-path",
5339 "apps/web/package.json",
5340 ]);
5341
5342 let Some(Commands::Publish(publish_cmd)) = cli.command else {
5343 panic!("expected publish command");
5344 };
5345
5346 assert!(publish_cmd.allow_dirty);
5347 assert!(publish_cmd.force);
5348 assert!(publish_cmd.include_prereqs);
5349 assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
5350 assert_eq!(publish_cmd.service.as_deref(), Some("web"));
5351 assert_eq!(
5352 publish_cmd.manifest_path,
5353 Some(PathBuf::from("apps/web/package.json"))
5354 );
5355 }
5356
5357 #[test]
5358 fn parses_npm_setup_release_config_command() {
5359 let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
5360
5361 let Some(Commands::Config(config_cmd)) = cli.command else {
5362 panic!("expected config command");
5363 };
5364 let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
5365 panic!("expected npm config command");
5366 };
5367
5368 assert!(matches!(
5369 registry_cmd.action,
5370 super::RegistryConfigAction::SetupRelease
5371 ));
5372 }
5373
5374 #[test]
5375 fn parses_release_setup_config_command() {
5376 let cli = Cli::parse_from(["xbp", "config", "release", "setup"]);
5377
5378 let Some(Commands::Config(config_cmd)) = cli.command else {
5379 panic!("expected config command");
5380 };
5381 let Some(super::ConfigProviderCmd::Release(release_cmd)) = config_cmd.provider else {
5382 panic!("expected release config command");
5383 };
5384
5385 assert!(matches!(
5386 release_cmd.action,
5387 super::ReleaseConfigAction::Setup
5388 ));
5389 }
5390
5391 #[test]
5392 fn parses_crates_login_config_command() {
5393 let cli = Cli::parse_from(["xbp", "config", "crates", "login"]);
5394
5395 let Some(Commands::Config(config_cmd)) = cli.command else {
5396 panic!("expected config command");
5397 };
5398 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
5399 panic!("expected crates config command");
5400 };
5401
5402 assert!(matches!(
5403 crates_cmd.action,
5404 super::CratesConfigAction::Login { .. }
5405 ));
5406 }
5407
5408 #[test]
5409 fn parses_crates_logout_config_command() {
5410 let cli = Cli::parse_from(["xbp", "config", "crates", "logout"]);
5411
5412 let Some(Commands::Config(config_cmd)) = cli.command else {
5413 panic!("expected config command");
5414 };
5415 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
5416 panic!("expected crates config command");
5417 };
5418
5419 assert!(matches!(
5420 crates_cmd.action,
5421 super::CratesConfigAction::Logout
5422 ));
5423 }
5424
5425 #[test]
5426 fn parses_whoami_command() {
5427 let cli = Cli::parse_from(["xbp", "whoami"]);
5428
5429 assert!(matches!(cli.command, Some(Commands::Whoami)));
5430 }
5431
5432 #[test]
5433 fn parses_shell_alias_as_ssh_command() {
5434 let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
5435
5436 let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
5437 panic!("expected ssh command through shell alias");
5438 };
5439
5440 assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
5441 }
5442
5443 #[test]
5444 fn parses_api_request_command() {
5445 let cli = Cli::parse_from([
5446 "xbp",
5447 "api",
5448 "request",
5449 "/api/registry/installers/python-pip",
5450 "--web",
5451 "--method",
5452 "GET",
5453 "--header",
5454 "accept: application/json",
5455 ]);
5456
5457 let Some(Commands::Api(api_cmd)) = cli.command else {
5458 panic!("expected api command");
5459 };
5460
5461 match api_cmd.command {
5462 super::ApiSubCommand::Request(request_cmd) => {
5463 assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
5464 assert!(request_cmd.target.web);
5465 assert_eq!(request_cmd.method.as_deref(), Some("GET"));
5466 assert_eq!(
5467 request_cmd.target.header,
5468 vec!["accept: application/json".to_string()]
5469 );
5470 }
5471 _ => panic!("expected api request subcommand"),
5472 }
5473 }
5474
5475 #[test]
5476 fn parses_api_projects_list_command() {
5477 let cli = Cli::parse_from([
5478 "xbp",
5479 "api",
5480 "projects",
5481 "list",
5482 "--organization-id",
5483 "org_123",
5484 ]);
5485
5486 let Some(Commands::Api(api_cmd)) = cli.command else {
5487 panic!("expected api command");
5488 };
5489
5490 match api_cmd.command {
5491 super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
5492 super::ApiProjectsSubCommand::List(list_cmd) => {
5493 assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
5494 }
5495 _ => panic!("expected projects list subcommand"),
5496 },
5497 _ => panic!("expected projects subcommand"),
5498 }
5499 }
5500
5501 #[test]
5502 fn parses_api_routes_create_command() {
5503 let cli = Cli::parse_from([
5504 "xbp",
5505 "api",
5506 "routes",
5507 "create",
5508 "--domain",
5509 "demo.local",
5510 "--target",
5511 "http://127.0.0.1:3000",
5512 "--weighted-target",
5513 "http://127.0.0.1:3001=3",
5514 "--base-url",
5515 "http://127.0.0.1:8080",
5516 ]);
5517
5518 let Some(Commands::Api(api_cmd)) = cli.command else {
5519 panic!("expected api command");
5520 };
5521
5522 match api_cmd.command {
5523 super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
5524 super::ApiRoutesSubCommand::Create(create_cmd) => {
5525 assert_eq!(create_cmd.domain, "demo.local");
5526 assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
5527 assert_eq!(
5528 create_cmd.weighted_target,
5529 vec!["http://127.0.0.1:3001=3".to_string()]
5530 );
5531 assert_eq!(
5532 create_cmd.target_options.base_url.as_deref(),
5533 Some("http://127.0.0.1:8080")
5534 );
5535 }
5536 _ => panic!("expected routes create subcommand"),
5537 },
5538 _ => panic!("expected routes subcommand"),
5539 }
5540 }
5541
5542 #[test]
5543 fn parses_hetzner_vswitch_setup_command() {
5544 let cli = Cli::parse_from([
5545 "xbp",
5546 "network",
5547 "hetzner",
5548 "vswitch",
5549 "setup",
5550 "--ip",
5551 "10.0.3.2",
5552 "--vlan-id",
5553 "4000",
5554 "--interface",
5555 "enp0s31f6",
5556 "--apply",
5557 ]);
5558
5559 let Some(Commands::Network(network_cmd)) = cli.command else {
5560 panic!("expected network command");
5561 };
5562
5563 match network_cmd.command {
5564 NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
5565 NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
5566 NetworkHetznerVswitchSubCommand::Setup {
5567 ip,
5568 cidr,
5569 interface,
5570 vlan_id,
5571 apply,
5572 ..
5573 } => {
5574 assert_eq!(ip, "10.0.3.2");
5575 assert_eq!(cidr, 24);
5576 assert_eq!(interface.as_deref(), Some("enp0s31f6"));
5577 assert_eq!(vlan_id, 4000);
5578 assert!(apply);
5579 }
5580 },
5581 },
5582 _ => panic!("expected hetzner subcommand"),
5583 }
5584 }
5585
5586 #[cfg(feature = "secrets")]
5587 #[test]
5588 fn parses_secrets_diag_command() {
5589 let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
5590
5591 match cli.command {
5592 Some(Commands::Secrets(secrets_cmd)) => {
5593 assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
5594 assert_eq!(secrets_cmd.environment, "xbp-dev");
5595 }
5596 _ => panic!("expected secrets command"),
5597 }
5598 }
5599
5600 #[cfg(feature = "secrets")]
5601 #[test]
5602 fn parses_secrets_environment_override() {
5603 let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
5604
5605 match cli.command {
5606 Some(Commands::Secrets(secrets_cmd)) => {
5607 assert_eq!(secrets_cmd.environment, "xbp-prod");
5608 assert!(matches!(
5609 secrets_cmd.command,
5610 Some(SecretsSubCommand::Push(_))
5611 ));
5612 }
5613 _ => panic!("expected secrets command"),
5614 }
5615 }
5616
5617 #[cfg(feature = "secrets")]
5618 #[test]
5619 fn parses_secrets_dev_vars_sync_flags() {
5620 let cli = Cli::parse_from([
5621 "xbp",
5622 "secrets",
5623 "--environment",
5624 "xbp-prod",
5625 "pull",
5626 "--include-dev-vars",
5627 "--dev-vars-output",
5628 ".dev.vars",
5629 ]);
5630
5631 match cli.command {
5632 Some(Commands::Secrets(secrets_cmd)) => {
5633 assert_eq!(secrets_cmd.environment, "xbp-prod");
5634 match secrets_cmd.command {
5635 Some(SecretsSubCommand::Pull(pull_cmd)) => {
5636 assert!(pull_cmd.include_dev_vars);
5637 assert_eq!(pull_cmd.dev_vars_output.as_deref(), Some(".dev.vars"));
5638 }
5639 _ => panic!("expected pull command"),
5640 }
5641 }
5642 _ => panic!("expected secrets command"),
5643 }
5644 }
5645
5646 #[test]
5647 fn parses_version_discover_command() {
5648 let cli = Cli::parse_from(["xbp", "version", "discover", "--dry-run"]);
5649
5650 match cli.command {
5651 Some(Commands::Version(version_cmd)) => match version_cmd.command {
5652 Some(super::VersionSubCommand::Discover(discover_cmd)) => {
5653 assert!(discover_cmd.dry_run);
5654 assert!(!discover_cmd.no_register);
5655 }
5656 _ => panic!("expected version discover subcommand"),
5657 },
5658 _ => panic!("expected version command"),
5659 }
5660 }
5661
5662 #[cfg(feature = "secrets")]
5663 #[test]
5664 fn parses_secrets_providers_command() {
5665 let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
5666
5667 match cli.command {
5668 Some(Commands::Secrets(secrets_cmd)) => {
5669 assert!(matches!(
5670 secrets_cmd.command,
5671 Some(SecretsSubCommand::Providers)
5672 ));
5673 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
5674 }
5675 _ => panic!("expected secrets command"),
5676 }
5677 }
5678
5679 #[cfg(feature = "secrets")]
5680 #[test]
5681 fn parses_cloudflare_secret_store_create() {
5682 let cli = Cli::parse_from([
5683 "xbp",
5684 "secrets",
5685 "--provider",
5686 "cloudflare",
5687 "stores",
5688 "create",
5689 "--name",
5690 "prod",
5691 ]);
5692
5693 match cli.command {
5694 Some(Commands::Secrets(secrets_cmd)) => {
5695 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
5696 match secrets_cmd.command {
5697 Some(SecretsSubCommand::Stores(stores_cmd)) => {
5698 assert!(matches!(
5699 stores_cmd.command,
5700 SecretsStoresSubCommand::Create(_)
5701 ));
5702 }
5703 _ => panic!("expected stores subcommand"),
5704 }
5705 }
5706 _ => panic!("expected secrets command"),
5707 }
5708 }
5709
5710 #[cfg(feature = "secrets")]
5711 #[test]
5712 fn parses_cloudflare_secret_duplicate() {
5713 let cli = Cli::parse_from([
5714 "xbp",
5715 "secrets",
5716 "--provider",
5717 "cloudflare",
5718 "secrets",
5719 "duplicate",
5720 "--store-id",
5721 "store_1",
5722 "--secret-id",
5723 "secret_1",
5724 "--name",
5725 "COPY",
5726 ]);
5727
5728 match cli.command {
5729 Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
5730 Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
5731 assert!(matches!(
5732 secrets_cmd.command,
5733 CloudflareSecretsSubCommand::Duplicate(_)
5734 ));
5735 }
5736 _ => panic!("expected cloudflare secrets subcommand"),
5737 },
5738 _ => panic!("expected secrets command"),
5739 }
5740 }
5741
5742 #[test]
5743 fn parses_workers_secret_put_from_stdin_command() {
5744 let cli = Cli::parse_from([
5745 "xbp",
5746 "workers",
5747 "secrets",
5748 "--environment",
5749 "production",
5750 "put",
5751 "--name",
5752 "API_KEY",
5753 "--from-stdin",
5754 ]);
5755
5756 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5757 panic!("expected workers command");
5758 };
5759
5760 match workers_cmd.command {
5761 super::WorkersSubCommand::Secrets(secrets_cmd) => {
5762 assert_eq!(
5763 secrets_cmd.target.environment.as_deref(),
5764 Some("production")
5765 );
5766 match secrets_cmd.command {
5767 super::WorkersSecretsSubCommand::Put(put_cmd) => {
5768 assert_eq!(put_cmd.name, "API_KEY");
5769 assert!(put_cmd.from_stdin);
5770 assert_eq!(put_cmd.value, None);
5771 }
5772 _ => panic!("expected workers secret put"),
5773 }
5774 }
5775 _ => panic!("expected workers secrets command"),
5776 }
5777 }
5778
5779 #[test]
5780 fn parses_workers_d1_migrations_local_command() {
5781 let cli = Cli::parse_from([
5782 "xbp",
5783 "workers",
5784 "d1",
5785 "migrations",
5786 "apply",
5787 "DB",
5788 "--local",
5789 "--environment",
5790 "preview",
5791 ]);
5792
5793 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5794 panic!("expected workers command");
5795 };
5796
5797 match workers_cmd.command {
5798 super::WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
5799 super::WorkersD1SubCommand::Migrations(migrations_cmd) => {
5800 match migrations_cmd.command {
5801 super::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
5802 assert_eq!(apply_cmd.database, "DB");
5803 assert!(apply_cmd.local);
5804 assert!(!apply_cmd.remote);
5805 assert_eq!(apply_cmd.target.environment.as_deref(), Some("preview"));
5806 }
5807 }
5808 }
5809 },
5810 _ => panic!("expected workers d1 command"),
5811 }
5812 }
5813
5814 #[test]
5815 fn parses_workers_wrangler_passthrough_for_r2_queues_and_secrets_store() {
5816 let cli = Cli::parse_from([
5817 "xbp",
5818 "workers",
5819 "wrangler",
5820 "run",
5821 "--",
5822 "r2",
5823 "bucket",
5824 "cors",
5825 "set",
5826 "r2-bucket",
5827 "--file",
5828 "cors.json",
5829 ]);
5830
5831 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5832 panic!("expected workers command");
5833 };
5834
5835 match workers_cmd.command {
5836 super::WorkersSubCommand::Wrangler(wrangler_cmd) => match wrangler_cmd.command {
5837 super::WorkersWranglerSubCommand::Run(run_cmd) => assert_eq!(
5838 run_cmd.args,
5839 [
5840 "r2",
5841 "bucket",
5842 "cors",
5843 "set",
5844 "r2-bucket",
5845 "--file",
5846 "cors.json"
5847 ]
5848 ),
5849 _ => panic!("expected Wrangler run command"),
5850 },
5851 _ => panic!("expected workers wrangler command"),
5852 }
5853 }
5854
5855 #[test]
5856 fn parses_workers_deploy_ci_version_upload_command() {
5857 let cli = Cli::parse_from(["xbp", "workers", "deploy", "ci", "--version-upload"]);
5858
5859 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5860 panic!("expected workers command");
5861 };
5862
5863 match workers_cmd.command {
5864 super::WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
5865 super::WorkersDeploySubCommand::Ci(ci_cmd) => {
5866 assert!(ci_cmd.version_upload);
5867 }
5868 _ => panic!("expected workers deploy ci command"),
5869 },
5870 _ => panic!("expected workers deploy command"),
5871 }
5872 }
5873
5874 #[test]
5875 fn parses_workers_list_alias_command() {
5876 let cli = Cli::parse_from(["xbp", "workers", "ls", "--all"]);
5877
5878 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5879 panic!("expected workers command");
5880 };
5881
5882 match workers_cmd.command {
5883 super::WorkersSubCommand::List(list_cmd) => {
5884 assert!(list_cmd.all);
5885 assert!(!list_cmd.json);
5886 }
5887 _ => panic!("expected workers list command"),
5888 }
5889 }
5890
5891 #[test]
5892 fn parses_workers_logs_follow_and_build_flags() {
5893 let cli = Cli::parse_from([
5894 "xbp",
5895 "workers",
5896 "logs",
5897 "-f",
5898 "--build",
5899 "--failed",
5900 "xbp-production",
5901 ]);
5902
5903 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5904 panic!("expected workers command");
5905 };
5906
5907 match workers_cmd.command {
5908 super::WorkersSubCommand::Logs(logs_cmd) => {
5909 assert!(logs_cmd.follow);
5910 assert!(logs_cmd.build);
5911 assert!(logs_cmd.failed);
5912 assert_eq!(logs_cmd.script_name.as_deref(), Some("xbp-production"));
5913 }
5914 _ => panic!("expected workers logs command"),
5915 }
5916 }
5917
5918 #[test]
5919 fn parses_workers_logs_worker_and_wait_flags() {
5920 let cli = Cli::parse_from([
5921 "xbp",
5922 "workers",
5923 "logs",
5924 "--worker",
5925 "suits-formations",
5926 "--json",
5927 "--wait",
5928 "--wait-seconds",
5929 "60",
5930 ]);
5931
5932 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5933 panic!("expected workers command");
5934 };
5935
5936 match workers_cmd.command {
5937 super::WorkersSubCommand::Logs(logs_cmd) => {
5938 assert_eq!(logs_cmd.target.worker.as_deref(), Some("suits-formations"));
5939 assert!(logs_cmd.json);
5940 assert!(logs_cmd.wait);
5941 assert_eq!(logs_cmd.wait_seconds, 60);
5942 }
5943 _ => panic!("expected workers logs command"),
5944 }
5945 }
5946
5947 #[test]
5948 fn parses_workers_logs_output_and_filter_flags() {
5949 let cli = Cli::parse_from([
5950 "xbp",
5951 "workers",
5952 "logs",
5953 "--build",
5954 "-n",
5955 "200",
5956 "-o",
5957 "build.log",
5958 "-g",
5959 "error",
5960 "--errors-only",
5961 "--list-builds",
5962 "--build-index",
5963 "1",
5964 ]);
5965
5966 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5967 panic!("expected workers command");
5968 };
5969
5970 match workers_cmd.command {
5971 super::WorkersSubCommand::Logs(logs_cmd) => {
5972 assert!(logs_cmd.build);
5973 assert_eq!(logs_cmd.lines, Some(200));
5974 assert_eq!(
5975 logs_cmd.output.as_deref().and_then(|path| path.to_str()),
5976 Some("build.log")
5977 );
5978 assert_eq!(logs_cmd.grep.as_deref(), Some("error"));
5979 assert!(logs_cmd.errors_only);
5980 assert!(logs_cmd.list_builds);
5981 assert_eq!(logs_cmd.build_index, Some(1));
5982 }
5983 _ => panic!("expected workers logs command"),
5984 }
5985 }
5986
5987 #[test]
5988 fn parses_workers_list_filter_and_limit_flags() {
5989 let cli = Cli::parse_from([
5990 "xbp", "workers", "list", "--failed", "-n", "5", "--sort", "modified",
5991 ]);
5992
5993 let Some(Commands::Workers(workers_cmd)) = cli.command else {
5994 panic!("expected workers command");
5995 };
5996
5997 match workers_cmd.command {
5998 super::WorkersSubCommand::List(list_cmd) => {
5999 assert!(list_cmd.failed);
6000 assert_eq!(list_cmd.limit, Some(5));
6001 assert_eq!(list_cmd.sort, "modified");
6002 }
6003 _ => panic!("expected workers list command"),
6004 }
6005 }
6006
6007 #[test]
6008 fn parses_worker_alias_command() {
6009 let cli = Cli::parse_from(["xbp", "worker", "env", "--show-values"]);
6010
6011 let Some(Commands::Workers(workers_cmd)) = cli.command else {
6012 panic!("expected workers command through alias");
6013 };
6014
6015 match workers_cmd.command {
6016 super::WorkersSubCommand::Env(env_cmd) => {
6017 assert!(env_cmd.show_values);
6018 }
6019 _ => panic!("expected workers env command"),
6020 }
6021 }
6022
6023 #[test]
6024 fn parses_dns_providers_command() {
6025 let cli = Cli::parse_from(["xbp", "dns", "providers"]);
6026
6027 match cli.command {
6028 Some(Commands::Dns(dns_cmd)) => {
6029 assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
6030 }
6031 _ => panic!("expected dns command"),
6032 }
6033 }
6034
6035 #[test]
6036 fn dns_zone_list_defaults_provider_to_cloudflare() {
6037 let cli = Cli::parse_from(["xbp", "dns", "zones", "list"]);
6038
6039 let Some(Commands::Dns(dns_cmd)) = cli.command else {
6040 panic!("expected dns command");
6041 };
6042
6043 match dns_cmd.command {
6044 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
6045 DnsZonesSubCommand::List(list_cmd) => {
6046 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
6047 }
6048 _ => panic!("expected zones list command"),
6049 },
6050 _ => panic!("expected zones command"),
6051 }
6052 }
6053
6054 #[test]
6055 fn dns_record_list_defaults_provider_to_cloudflare() {
6056 let cli = Cli::parse_from(["xbp", "dns", "records", "list", "--zone-id", "zone_123"]);
6057
6058 let Some(Commands::Dns(dns_cmd)) = cli.command else {
6059 panic!("expected dns command");
6060 };
6061
6062 match dns_cmd.command {
6063 DnsSubCommand::Records(records_cmd) => match records_cmd.command {
6064 super::DnsRecordsSubCommand::List(list_cmd) => {
6065 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
6066 assert_eq!(list_cmd.zone_id, "zone_123");
6067 }
6068 _ => panic!("expected records list command"),
6069 },
6070 _ => panic!("expected records command"),
6071 }
6072 }
6073
6074 #[test]
6075 fn dns_help_includes_descriptions_and_examples() {
6076 let err = Cli::try_parse_from(["xbp", "dns", "-h"]).expect_err("help");
6077 let rendered = err.to_string();
6078
6079 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
6080 assert!(rendered.contains("Manage DNS providers, zones, records, DNSSEC, and settings"));
6081 assert!(rendered.contains("List supported DNS providers and current implementation status"));
6082 assert!(rendered.contains("xbp dns records create"));
6083 }
6084
6085 #[test]
6086 fn dns_providers_help_includes_discovery_note() {
6087 let err = Cli::try_parse_from(["xbp", "dns", "providers", "-h"]).expect_err("help");
6088 let rendered = err.to_string();
6089
6090 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
6091 assert!(rendered.contains("Implemented providers are wired into `xbp dns` today."));
6092 }
6093
6094 #[test]
6095 fn dns_records_without_subcommand_displays_help_screen() {
6096 let err = Cli::try_parse_from(["xbp", "dns", "records"]).expect_err("missing subcommand");
6097 let rendered = err.to_string();
6098
6099 assert!(matches!(
6100 err.kind(),
6101 clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
6102 | clap::error::ErrorKind::MissingSubcommand
6103 ));
6104 assert!(rendered.contains("List, create, edit, import, export, and batch DNS records"));
6105 assert!(rendered.contains("Create a new DNS record"));
6106 assert!(rendered.contains("xbp dns records import"));
6107 }
6108
6109 #[test]
6110 fn parses_dns_zone_list_command() {
6111 let cli = Cli::parse_from([
6112 "xbp",
6113 "dns",
6114 "zones",
6115 "list",
6116 "--provider",
6117 "cloudflare",
6118 "--account-name-op",
6119 "contains",
6120 "--type",
6121 "full,partial",
6122 ]);
6123
6124 match cli.command {
6125 Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
6126 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
6127 DnsZonesSubCommand::List(list_cmd) => {
6128 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
6129 assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
6130 assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
6131 }
6132 _ => panic!("expected dns zones list"),
6133 },
6134 _ => panic!("expected dns zones"),
6135 },
6136 _ => panic!("expected dns command"),
6137 }
6138 }
6139
6140 #[test]
6141 fn parses_domains_search_command() {
6142 let cli = Cli::parse_from([
6143 "xbp",
6144 "domains",
6145 "search",
6146 "--query",
6147 "xbp",
6148 "--extension",
6149 "com",
6150 ]);
6151
6152 match cli.command {
6153 Some(Commands::Domains(domains_cmd)) => {
6154 assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
6155 assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
6156 }
6157 _ => panic!("expected domains command"),
6158 }
6159 }
6160
6161 #[test]
6162 fn parses_cloudflare_config_account_id_command() {
6163 let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
6164
6165 match cli.command {
6166 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
6167 Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
6168 assert!(matches!(
6169 cloudflare_cmd.action,
6170 Some(CloudflareConfigAction::SetAccountId { .. })
6171 ));
6172 }
6173 _ => panic!("expected cloudflare config provider"),
6174 },
6175 _ => panic!("expected config command"),
6176 }
6177 }
6178
6179 #[test]
6180 fn parses_runners_groups_update_command() {
6181 let cli = Cli::parse_from([
6182 "xbp",
6183 "runners",
6184 "groups",
6185 "update",
6186 "--organization-id",
6187 "org_123",
6188 "--name",
6189 "linux-builders",
6190 "--visibility",
6191 "selected",
6192 "--restricted-to-workflows",
6193 ]);
6194
6195 let Some(Commands::Runners(runners_cmd)) = cli.command else {
6196 panic!("expected runners command");
6197 };
6198
6199 match runners_cmd.command {
6200 super::RunnersSubCommand::Groups(groups_cmd) => match groups_cmd.command {
6201 super::RunnersGroupsSubCommand::Update(update_cmd) => {
6202 assert_eq!(update_cmd.organization_id, "org_123");
6203 assert_eq!(update_cmd.name, "linux-builders");
6204 assert_eq!(update_cmd.visibility.as_deref(), Some("selected"));
6205 assert!(update_cmd.restricted_to_workflows);
6206 }
6207 _ => panic!("expected runners groups update command"),
6208 },
6209 _ => panic!("expected runners groups command"),
6210 }
6211 }
6212
6213 #[test]
6214 fn parses_api_runners_group_repositories_create_command() {
6215 let cli = Cli::parse_from([
6216 "xbp",
6217 "api",
6218 "runners",
6219 "group-repositories-create",
6220 "group_123",
6221 "--repository-owner",
6222 "xylex-group",
6223 "--repository-name",
6224 "xbp",
6225 "--repository-full-name",
6226 "xylex-group/xbp",
6227 "--is-private",
6228 ]);
6229
6230 let Some(Commands::Api(api_cmd)) = cli.command else {
6231 panic!("expected api command");
6232 };
6233
6234 match api_cmd.command {
6235 super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
6236 super::ApiRunnersSubCommand::GroupRepositoriesCreate(create_cmd) => {
6237 assert_eq!(create_cmd.runner_group_id, "group_123");
6238 assert_eq!(create_cmd.repository_owner, "xylex-group");
6239 assert_eq!(create_cmd.repository_name, "xbp");
6240 assert_eq!(create_cmd.repository_full_name, "xylex-group/xbp");
6241 assert!(create_cmd.is_private);
6242 }
6243 _ => panic!("expected api runners group repositories create command"),
6244 },
6245 _ => panic!("expected api runners command"),
6246 }
6247 }
6248
6249 #[test]
6250 fn parses_runners_host_preflight_command() {
6251 let cli = Cli::parse_from([
6252 "xbp",
6253 "runners",
6254 "hosts",
6255 "preflight",
6256 "--runner-host-id",
6257 "host_123",
6258 "--write-api",
6259 ]);
6260
6261 let Some(Commands::Runners(runners_cmd)) = cli.command else {
6262 panic!("expected runners command");
6263 };
6264
6265 match runners_cmd.command {
6266 super::RunnersSubCommand::Hosts(hosts_cmd) => match hosts_cmd.command {
6267 super::RunnersHostsSubCommand::Preflight(preflight_cmd) => {
6268 assert_eq!(preflight_cmd.runner_host_id, "host_123");
6269 assert!(preflight_cmd.write_api);
6270 }
6271 _ => panic!("expected runners hosts preflight command"),
6272 },
6273 _ => panic!("expected runners hosts command"),
6274 }
6275 }
6276
6277 #[test]
6278 fn parses_runners_agent_serve_command() {
6279 let cli = Cli::parse_from([
6280 "xbp",
6281 "runners",
6282 "agent",
6283 "serve",
6284 "--runner-host-id",
6285 "host_123",
6286 "--interval-seconds",
6287 "5",
6288 "--once",
6289 ]);
6290
6291 let Some(Commands::Runners(runners_cmd)) = cli.command else {
6292 panic!("expected runners command");
6293 };
6294
6295 match runners_cmd.command {
6296 super::RunnersSubCommand::Agent(agent_cmd) => match agent_cmd.command {
6297 super::RunnersAgentSubCommand::Serve(serve_cmd) => {
6298 assert_eq!(serve_cmd.runner_host_id, "host_123");
6299 assert_eq!(serve_cmd.interval_seconds, 5);
6300 assert!(serve_cmd.once);
6301 }
6302 },
6303 _ => panic!("expected runners agent command"),
6304 }
6305 }
6306
6307 #[test]
6308 fn parses_api_runners_host_preflight_upsert_command() {
6309 let cli = Cli::parse_from([
6310 "xbp",
6311 "api",
6312 "runners",
6313 "hosts-preflights-upsert",
6314 "--runner-host-id",
6315 "host_123",
6316 "--platform",
6317 "linux",
6318 "--status",
6319 "synced",
6320 "--service-manager",
6321 "systemd",
6322 ]);
6323
6324 let Some(Commands::Api(api_cmd)) = cli.command else {
6325 panic!("expected api command");
6326 };
6327
6328 match api_cmd.command {
6329 super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
6330 super::ApiRunnersSubCommand::HostsPreflightsUpsert(upsert_cmd) => {
6331 assert_eq!(upsert_cmd.runner_host_id, "host_123");
6332 assert_eq!(upsert_cmd.platform, "linux");
6333 assert_eq!(upsert_cmd.status, "synced");
6334 assert_eq!(upsert_cmd.service_manager.as_deref(), Some("systemd"));
6335 }
6336 _ => panic!("expected api runners host preflight upsert command"),
6337 },
6338 _ => panic!("expected api runners command"),
6339 }
6340 }
6341}