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