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