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