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