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