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 start --parent C:\\Users\\floris\\Documents\\GitHub --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 conflicts_with = "parent",
1753 help = "Git repository root to watch; defaults to the current repo"
1754 )]
1755 pub repo: Option<PathBuf>,
1756 #[arg(
1757 long,
1758 conflicts_with = "repo",
1759 help = "Folder containing git repositories to target recursively"
1760 )]
1761 pub parent: Option<PathBuf>,
1762}
1763
1764#[derive(Args, Debug)]
1765pub struct WorktreeWatchStartCmd {
1766 #[command(flatten)]
1767 pub target: WorktreeWatchTarget,
1768 #[arg(long, help = "Spawn the watcher as a detached background process")]
1769 pub detach: bool,
1770 #[arg(
1771 long,
1772 default_value_t = 60,
1773 help = "Upload queued events every N seconds while watching; set 0 to disable periodic sync"
1774 )]
1775 pub sync_interval_seconds: u64,
1776 #[arg(long, help = "Record current git commit state once and exit")]
1777 pub once: bool,
1778}
1779
1780#[derive(Args, Debug)]
1781pub struct WorktreeWatchSyncCmd {
1782 #[command(flatten)]
1783 pub target: WorktreeWatchTarget,
1784 #[arg(long, help = "Print the upload plan without sending it to xbp.app")]
1785 pub dry_run: bool,
1786 #[arg(
1787 long,
1788 help = "Upload all local JSONL spool files, including files already marked synced"
1789 )]
1790 pub resync: bool,
1791}
1792
1793#[derive(Args, Debug)]
1794pub struct WorktreeWatchStatusCmd {
1795 #[command(flatten)]
1796 pub target: WorktreeWatchTarget,
1797 #[arg(long, help = "Emit status as JSON")]
1798 pub json: bool,
1799 #[arg(
1800 long,
1801 help = "Print decoded mutation and commit records from local JSONL spool files"
1802 )]
1803 pub records: bool,
1804 #[arg(
1805 long,
1806 default_value_t = 50,
1807 requires = "records",
1808 help = "Maximum decoded JSONL records to print per repository"
1809 )]
1810 pub record_limit: usize,
1811 #[arg(
1812 long,
1813 help = "Generate and print persisted activity statistics from local JSONL spool files"
1814 )]
1815 pub stats: bool,
1816 #[arg(
1817 long,
1818 default_value_t = 15,
1819 requires = "stats",
1820 help = "Maximum minutes between mutation events counted as one coding session"
1821 )]
1822 pub stats_gap_minutes: u64,
1823}
1824
1825#[derive(Args, Debug, Clone, Default)]
1826pub struct ApiTargetOptions {
1827 #[arg(long, help = "Override the request base URL for this command")]
1828 pub base_url: Option<String>,
1829 #[arg(
1830 long,
1831 help = "Target the hosted web origin (xbp.app) instead of the configured API_XBP_URL base"
1832 )]
1833 pub web: bool,
1834 #[arg(
1835 long,
1836 help = "Skip bearer token auth even when XBP_API_TOKEN is configured"
1837 )]
1838 pub no_auth: bool,
1839 #[arg(
1840 long,
1841 help = "Extra header in 'Name: Value' format",
1842 value_name = "HEADER"
1843 )]
1844 pub header: Vec<String>,
1845 #[arg(long, help = "Print response headers")]
1846 pub include_headers: bool,
1847 #[arg(
1848 long,
1849 help = "Print the response body as-is without JSON pretty formatting"
1850 )]
1851 pub raw: bool,
1852}
1853
1854#[cfg(feature = "docker")]
1855#[derive(Args, Debug)]
1856pub struct DockerCmd {
1857 #[arg(
1858 trailing_var_arg = true,
1859 allow_hyphen_values = true,
1860 help = "Arguments to pass directly to the Docker CLI (default: --help)"
1861 )]
1862 pub args: Vec<String>,
1863}
1864
1865#[derive(Subcommand, Debug)]
1866pub enum ApiSubCommand {
1867 #[command(about = "Install and enable the local xbp-api.service on Linux/systemd")]
1868 Install {
1869 #[arg(long, default_value_t = 8080, help = "Port to expose the API on")]
1870 port: u16,
1871 },
1872 #[command(about = "Call the XBP API health endpoint")]
1873 Health(ApiHealthCmd),
1874 #[command(about = "Manage XBP control-plane projects")]
1875 Projects(ApiProjectsCmd),
1876 #[command(about = "Manage XBP daemon registrations and heartbeats")]
1877 Daemons(ApiDaemonsCmd),
1878 #[command(about = "Manage XBP deployment jobs")]
1879 Jobs(ApiJobsCmd),
1880 #[command(about = "Manage XBP runner hosts, groups, inventory, and runner jobs")]
1881 Runners(ApiRunnersCmd),
1882 #[command(about = "Manage XBP proxy routes on the local API server")]
1883 Routes(ApiRoutesCmd),
1884 #[command(about = "Send an authenticated HTTP request to the configured XBP API surface")]
1885 Request(ApiRequestCmd),
1886}
1887
1888#[derive(Args, Debug)]
1889pub struct ApiHealthCmd {
1890 #[command(flatten)]
1891 pub target: ApiTargetOptions,
1892}
1893
1894#[derive(Args, Debug)]
1895pub struct ApiProjectsCmd {
1896 #[command(subcommand)]
1897 pub command: ApiProjectsSubCommand,
1898}
1899
1900#[derive(Subcommand, Debug)]
1901pub enum ApiProjectsSubCommand {
1902 #[command(about = "List projects from the XBP control-plane API")]
1903 List(ApiProjectsListCmd),
1904 #[command(about = "Create or upsert a control-plane project")]
1905 Create(Box<ApiProjectsCreateCmd>),
1906}
1907
1908#[derive(Args, Debug)]
1909pub struct ApiProjectsListCmd {
1910 #[arg(long, help = "Optional organization ID filter")]
1911 pub organization_id: Option<String>,
1912 #[command(flatten)]
1913 pub target: ApiTargetOptions,
1914}
1915
1916#[derive(Args, Debug)]
1917pub struct ApiProjectsCreateCmd {
1918 #[arg(long, help = "Project name")]
1919 pub name: String,
1920 #[arg(long, help = "Project path or repo path key")]
1921 pub path: String,
1922 #[arg(long, help = "Optional organization ID")]
1923 pub organization_id: Option<String>,
1924 #[arg(long, help = "Optional project slug")]
1925 pub slug: Option<String>,
1926 #[arg(long, help = "Optional project version")]
1927 pub version: Option<String>,
1928 #[arg(long, help = "Optional build directory")]
1929 pub build_dir: Option<String>,
1930 #[arg(long, help = "Optional runtime enum value")]
1931 pub runtime: Option<String>,
1932 #[arg(long, help = "Optional default branch")]
1933 pub default_branch: Option<String>,
1934 #[arg(long, help = "Optional repository root directory")]
1935 pub root_directory: Option<String>,
1936 #[arg(long, help = "Optional build command")]
1937 pub build_command: Option<String>,
1938 #[arg(long, help = "Optional install command")]
1939 pub install_command: Option<String>,
1940 #[arg(long, help = "Optional start command")]
1941 pub start_command: Option<String>,
1942 #[arg(long, help = "Optional output directory")]
1943 pub output_directory: Option<String>,
1944 #[arg(long, help = "Repository JSON payload matching GitRepositoryRef")]
1945 pub repository_json: Option<String>,
1946 #[arg(long, help = "Runtime policy JSON payload")]
1947 pub runtime_policy_json: Option<String>,
1948 #[arg(long, help = "Metadata JSON object")]
1949 pub metadata_json: Option<String>,
1950 #[command(flatten)]
1951 pub target: ApiTargetOptions,
1952}
1953
1954#[derive(Args, Debug)]
1955pub struct ApiDaemonsCmd {
1956 #[command(subcommand)]
1957 pub command: ApiDaemonsSubCommand,
1958}
1959
1960#[derive(Subcommand, Debug)]
1961pub enum ApiDaemonsSubCommand {
1962 #[command(about = "List registered daemons")]
1963 List(ApiDaemonsListCmd),
1964 #[command(about = "Register or upsert a daemon record")]
1965 Register(ApiDaemonsRegisterCmd),
1966 #[command(about = "Post a heartbeat update for a daemon")]
1967 Heartbeat(ApiDaemonsHeartbeatCmd),
1968 #[command(about = "Update daemon status only")]
1969 UpdateStatus(ApiDaemonsUpdateStatusCmd),
1970}
1971
1972#[derive(Args, Debug)]
1973pub struct ApiDaemonsListCmd {
1974 #[command(flatten)]
1975 pub target: ApiTargetOptions,
1976}
1977
1978#[derive(Args, Debug)]
1979pub struct ApiDaemonsRegisterCmd {
1980 #[arg(long, help = "Daemon node name")]
1981 pub node_name: String,
1982 #[arg(long, help = "Daemon hostname")]
1983 pub hostname: String,
1984 #[arg(long, help = "Daemon binary version")]
1985 pub version: String,
1986 #[arg(long, help = "Optional region")]
1987 pub region: Option<String>,
1988 #[arg(long, help = "Optional public IP")]
1989 pub public_ip: Option<String>,
1990 #[arg(long, help = "Optional internal IP")]
1991 pub internal_ip: Option<String>,
1992 #[arg(long, help = "Optional status enum value")]
1993 pub status: Option<String>,
1994 #[arg(long, help = "Optional CPU core count")]
1995 pub cpu_cores: Option<i32>,
1996 #[arg(long, help = "Optional total memory in MB")]
1997 pub memory_total_mb: Option<i32>,
1998 #[arg(long, help = "Optional total disk in GB")]
1999 pub disk_total_gb: Option<i32>,
2000 #[arg(long, help = "Labels JSON object")]
2001 pub labels_json: Option<String>,
2002 #[arg(long, help = "Metadata JSON object")]
2003 pub metadata_json: Option<String>,
2004 #[command(flatten)]
2005 pub target: ApiTargetOptions,
2006}
2007
2008#[derive(Args, Debug)]
2009pub struct ApiDaemonsHeartbeatCmd {
2010 #[arg(help = "Daemon ID")]
2011 pub daemon_id: String,
2012 #[arg(long, help = "Optional status enum value")]
2013 pub status: Option<String>,
2014 #[arg(long, help = "Optional daemon version")]
2015 pub version: Option<String>,
2016 #[arg(long, help = "Optional public IP")]
2017 pub public_ip: Option<String>,
2018 #[arg(long, help = "Optional internal IP")]
2019 pub internal_ip: Option<String>,
2020 #[arg(long, help = "Optional CPU core count")]
2021 pub cpu_cores: Option<i32>,
2022 #[arg(long, help = "Optional total memory in MB")]
2023 pub memory_total_mb: Option<i32>,
2024 #[arg(long, help = "Optional total disk in GB")]
2025 pub disk_total_gb: Option<i32>,
2026 #[arg(long, help = "Labels JSON object")]
2027 pub labels_json: Option<String>,
2028 #[command(flatten)]
2029 pub target: ApiTargetOptions,
2030}
2031
2032#[derive(Args, Debug)]
2033pub struct ApiDaemonsUpdateStatusCmd {
2034 #[arg(help = "Daemon ID")]
2035 pub daemon_id: String,
2036 #[arg(long, help = "Daemon status enum value")]
2037 pub status: String,
2038 #[command(flatten)]
2039 pub target: ApiTargetOptions,
2040}
2041
2042#[derive(Args, Debug)]
2043pub struct ApiJobsCmd {
2044 #[command(subcommand)]
2045 pub command: ApiJobsSubCommand,
2046}
2047
2048#[derive(Args, Debug)]
2049pub struct ApiRunnersCmd {
2050 #[command(subcommand)]
2051 pub command: ApiRunnersSubCommand,
2052}
2053
2054#[derive(Subcommand, Debug)]
2055pub enum ApiRunnersSubCommand {
2056 #[command(about = "List runner hosts")]
2057 HostsList(ApiRunnersHostsListCmd),
2058 #[command(about = "Register or upsert a runner host")]
2059 HostsEnroll(ApiRunnersHostsEnrollCmd),
2060 #[command(about = "Post a heartbeat update for a runner host")]
2061 HostsHeartbeat(ApiRunnersHostsHeartbeatCmd),
2062 #[command(about = "List persisted preflight records for a runner host")]
2063 HostsPreflightsList(ApiRunnersHostsPreflightsListCmd),
2064 #[command(about = "Create or replace a persisted preflight record for a runner host")]
2065 HostsPreflightsUpsert(ApiRunnersHostsPreflightsUpsertCmd),
2066 #[command(about = "List runner groups for an organization")]
2067 GroupsList(ApiRunnersGroupsListCmd),
2068 #[command(about = "Create or upsert a runner group")]
2069 GroupsCreate(ApiRunnersGroupsCreateCmd),
2070 #[command(about = "Update an existing runner group")]
2071 GroupsUpdate(ApiRunnersGroupsCreateCmd),
2072 #[command(about = "Delete a runner group")]
2073 GroupsDelete(ApiRunnersGroupsDeleteCmd),
2074 #[command(about = "List repository access for a runner group")]
2075 GroupRepositoriesList(ApiRunnersGroupRepositoriesListCmd),
2076 #[command(about = "Grant or upsert repository access for a runner group")]
2077 GroupRepositoriesCreate(ApiRunnersGroupRepositoriesCreateCmd),
2078 #[command(about = "List runner inventory for an organization")]
2079 InventoryList(ApiRunnersInventoryListCmd),
2080 #[command(about = "Upsert a runner inventory record")]
2081 InventoryUpsert(ApiRunnersInventoryUpsertCmd),
2082 #[command(about = "List runner jobs")]
2083 JobsList(ApiRunnersJobsListCmd),
2084 #[command(about = "Create a runner job")]
2085 JobsCreate(ApiRunnersJobsCreateCmd),
2086 #[command(about = "Claim the next runner job")]
2087 JobsClaim(ApiRunnersJobsClaimCmd),
2088 #[command(about = "Update a runner job")]
2089 JobsUpdate(ApiRunnersJobsUpdateCmd),
2090}
2091
2092#[derive(Args, Debug)]
2093pub struct ApiRunnersHostsListCmd {
2094 #[arg(long)]
2095 pub organization_id: Option<String>,
2096 #[arg(long)]
2097 pub daemon_id: Option<String>,
2098 #[arg(long)]
2099 pub status: Option<String>,
2100 #[command(flatten)]
2101 pub target: ApiTargetOptions,
2102}
2103
2104#[derive(Args, Debug)]
2105pub struct ApiRunnersHostsEnrollCmd {
2106 #[arg(long)]
2107 pub organization_id: String,
2108 #[arg(long)]
2109 pub daemon_id: Option<String>,
2110 #[arg(long)]
2111 pub host_kind: String,
2112 #[arg(long)]
2113 pub platform: String,
2114 #[arg(long)]
2115 pub hostname: String,
2116 #[arg(long)]
2117 pub runner_name_prefix: String,
2118 #[arg(long)]
2119 pub display_name: Option<String>,
2120 #[arg(long)]
2121 pub arch: Option<String>,
2122 #[arg(long)]
2123 pub status: Option<String>,
2124 #[arg(long)]
2125 pub labels_json: Option<String>,
2126 #[arg(long)]
2127 pub capabilities_json: Option<String>,
2128 #[arg(long)]
2129 pub metadata_json: Option<String>,
2130 #[command(flatten)]
2131 pub target: ApiTargetOptions,
2132}
2133
2134#[derive(Args, Debug)]
2135pub struct ApiRunnersHostsHeartbeatCmd {
2136 #[arg(help = "Runner host ID")]
2137 pub runner_host_id: String,
2138 #[arg(long)]
2139 pub status: Option<String>,
2140 #[arg(long)]
2141 pub labels_json: Option<String>,
2142 #[arg(long)]
2143 pub capabilities_json: Option<String>,
2144 #[command(flatten)]
2145 pub target: ApiTargetOptions,
2146}
2147
2148#[derive(Args, Debug)]
2149pub struct ApiRunnersHostsPreflightsListCmd {
2150 #[arg(long)]
2151 pub runner_host_id: String,
2152 #[command(flatten)]
2153 pub target: ApiTargetOptions,
2154}
2155
2156#[derive(Args, Debug)]
2157pub struct ApiRunnersHostsPreflightsUpsertCmd {
2158 #[arg(long)]
2159 pub runner_host_id: String,
2160 #[arg(long)]
2161 pub platform: String,
2162 #[arg(long)]
2163 pub status: String,
2164 #[arg(long)]
2165 pub docker_available: Option<bool>,
2166 #[arg(long)]
2167 pub service_manager: Option<String>,
2168 #[arg(long)]
2169 pub checks_json: Option<String>,
2170 #[arg(long)]
2171 pub checked_at: Option<String>,
2172 #[command(flatten)]
2173 pub target: ApiTargetOptions,
2174}
2175
2176#[derive(Args, Debug, Clone)]
2177pub struct ApiRunnersGroupsListCmd {
2178 #[arg(long)]
2179 pub organization_id: String,
2180 #[command(flatten)]
2181 pub target: ApiTargetOptions,
2182}
2183
2184#[derive(Args, Debug, Clone)]
2185pub struct ApiRunnersGroupsCreateCmd {
2186 #[arg(long)]
2187 pub organization_id: String,
2188 #[arg(long)]
2189 pub name: String,
2190 #[arg(long)]
2191 pub visibility: Option<String>,
2192 #[arg(long)]
2193 pub github_installation_id: Option<String>,
2194 #[arg(long)]
2195 pub github_runner_group_id: Option<i64>,
2196 #[arg(long)]
2197 pub sync_state: Option<String>,
2198 #[arg(long)]
2199 pub sync_error: Option<String>,
2200 #[arg(long)]
2201 pub inherited: bool,
2202 #[arg(long)]
2203 pub allows_public_repositories: bool,
2204 #[arg(long)]
2205 pub interactive: bool,
2206 #[arg(long)]
2207 pub restricted_to_workflows: bool,
2208 #[arg(long)]
2209 pub selected_workflows_json: Option<String>,
2210 #[arg(long)]
2211 pub metadata_json: Option<String>,
2212 #[command(flatten)]
2213 pub target: ApiTargetOptions,
2214}
2215
2216#[derive(Args, Debug)]
2217pub struct ApiRunnersGroupsDeleteCmd {
2218 #[arg(help = "Runner group ID")]
2219 pub runner_group_id: String,
2220 #[command(flatten)]
2221 pub target: ApiTargetOptions,
2222}
2223
2224#[derive(Args, Debug)]
2225pub struct ApiRunnersGroupRepositoriesListCmd {
2226 #[arg(help = "Runner group ID")]
2227 pub runner_group_id: String,
2228 #[command(flatten)]
2229 pub target: ApiTargetOptions,
2230}
2231
2232#[derive(Args, Debug)]
2233pub struct ApiRunnersGroupRepositoriesCreateCmd {
2234 #[arg(help = "Runner group ID")]
2235 pub runner_group_id: String,
2236 #[arg(long)]
2237 pub github_repository_id: Option<i64>,
2238 #[arg(long)]
2239 pub repository_owner: String,
2240 #[arg(long)]
2241 pub repository_name: String,
2242 #[arg(long)]
2243 pub repository_full_name: String,
2244 #[arg(long)]
2245 pub is_private: bool,
2246 #[command(flatten)]
2247 pub target: ApiTargetOptions,
2248}
2249
2250#[derive(Args, Debug)]
2251pub struct ApiRunnersInventoryListCmd {
2252 #[arg(long)]
2253 pub organization_id: String,
2254 #[command(flatten)]
2255 pub target: ApiTargetOptions,
2256}
2257
2258#[derive(Args, Debug)]
2259pub struct ApiRunnersInventoryUpsertCmd {
2260 #[arg(long)]
2261 pub organization_id: String,
2262 #[arg(long)]
2263 pub runner_host_id: Option<String>,
2264 #[arg(long)]
2265 pub runner_group_id: Option<String>,
2266 #[arg(long)]
2267 pub github_runner_id: Option<i64>,
2268 #[arg(long)]
2269 pub name: String,
2270 #[arg(long)]
2271 pub platform: String,
2272 #[arg(long)]
2273 pub os: Option<String>,
2274 #[arg(long)]
2275 pub architecture: Option<String>,
2276 #[arg(long)]
2277 pub status: Option<String>,
2278 #[arg(long)]
2279 pub busy: bool,
2280 #[arg(long)]
2281 pub labels_json: Option<String>,
2282 #[arg(long)]
2283 pub metadata_json: Option<String>,
2284 #[command(flatten)]
2285 pub target: ApiTargetOptions,
2286}
2287
2288#[derive(Args, Debug, Clone)]
2289pub struct ApiRunnersJobsListCmd {
2290 #[arg(long)]
2291 pub organization_id: Option<String>,
2292 #[arg(long)]
2293 pub runner_host_id: Option<String>,
2294 #[arg(long)]
2295 pub runner_id: Option<String>,
2296 #[arg(long)]
2297 pub daemon_id: Option<String>,
2298 #[arg(long)]
2299 pub status: Option<String>,
2300 #[arg(long)]
2301 pub phase: Option<String>,
2302 #[arg(long)]
2303 pub limit: Option<usize>,
2304 #[command(flatten)]
2305 pub target: ApiTargetOptions,
2306}
2307
2308#[derive(Args, Debug)]
2309pub struct ApiRunnersJobsCreateCmd {
2310 #[arg(long)]
2311 pub organization_id: String,
2312 #[arg(long)]
2313 pub runner_host_id: Option<String>,
2314 #[arg(long)]
2315 pub daemon_id: Option<String>,
2316 #[arg(long)]
2317 pub runner_id: Option<String>,
2318 #[arg(long)]
2319 pub job_kind: String,
2320 #[arg(long)]
2321 pub priority: Option<i32>,
2322 #[arg(long)]
2323 pub max_attempts: Option<i32>,
2324 #[arg(long)]
2325 pub run_after: Option<String>,
2326 #[arg(long)]
2327 pub payload_json: Option<String>,
2328 #[command(flatten)]
2329 pub target: ApiTargetOptions,
2330}
2331
2332#[derive(Args, Debug)]
2333pub struct ApiRunnersJobsClaimCmd {
2334 #[arg(long)]
2335 pub runner_host_id: Option<String>,
2336 #[arg(long)]
2337 pub daemon_id: Option<String>,
2338 #[arg(long)]
2339 pub locked_by: Option<String>,
2340 #[command(flatten)]
2341 pub target: ApiTargetOptions,
2342}
2343
2344#[derive(Args, Debug)]
2345pub struct ApiRunnersJobsUpdateCmd {
2346 #[arg(help = "Runner job ID")]
2347 pub runner_job_id: String,
2348 #[arg(long)]
2349 pub status: String,
2350 #[arg(long)]
2351 pub error_text: Option<String>,
2352 #[command(flatten)]
2353 pub target: ApiTargetOptions,
2354}
2355
2356#[derive(Subcommand, Debug)]
2357pub enum ApiJobsSubCommand {
2358 #[command(about = "List deployment jobs")]
2359 List(ApiJobsListCmd),
2360 #[command(about = "Create a deployment job for a project")]
2361 Create(ApiJobsCreateCmd),
2362 #[command(about = "Claim the next deployment job for a daemon")]
2363 Claim(ApiJobsClaimCmd),
2364 #[command(about = "Update deployment job status")]
2365 Update(ApiJobsUpdateCmd),
2366}
2367
2368#[derive(Args, Debug)]
2369pub struct ApiJobsListCmd {
2370 #[arg(long, help = "Optional project ID filter")]
2371 pub project_id: Option<String>,
2372 #[arg(long, help = "Optional deployment ID filter")]
2373 pub deployment_id: Option<String>,
2374 #[arg(long, help = "Optional daemon ID filter")]
2375 pub daemon_id: Option<String>,
2376 #[arg(long, help = "Optional status filter")]
2377 pub status: Option<String>,
2378 #[arg(long, help = "Optional result limit")]
2379 pub limit: Option<usize>,
2380 #[command(flatten)]
2381 pub target: ApiTargetOptions,
2382}
2383
2384#[derive(Args, Debug)]
2385pub struct ApiJobsCreateCmd {
2386 #[arg(long, help = "Project ID")]
2387 pub project_id: String,
2388 #[arg(long, help = "Deployment ID")]
2389 pub deployment_id: String,
2390 #[arg(long, help = "Optional daemon ID assignment")]
2391 pub daemon_id: Option<String>,
2392 #[arg(long, help = "Optional priority")]
2393 pub priority: Option<i32>,
2394 #[arg(long, help = "Optional max attempts")]
2395 pub max_attempts: Option<i32>,
2396 #[arg(long, help = "Optional RFC3339 run-after timestamp")]
2397 pub run_after: Option<String>,
2398 #[arg(long, help = "Optional payload JSON object")]
2399 pub payload_json: Option<String>,
2400 #[command(flatten)]
2401 pub target: ApiTargetOptions,
2402}
2403
2404#[derive(Args, Debug)]
2405pub struct ApiJobsClaimCmd {
2406 #[arg(long, help = "Daemon ID claiming work")]
2407 pub daemon_id: String,
2408 #[arg(long, help = "Optional lock owner")]
2409 pub locked_by: Option<String>,
2410 #[command(flatten)]
2411 pub target: ApiTargetOptions,
2412}
2413
2414#[derive(Args, Debug)]
2415pub struct ApiJobsUpdateCmd {
2416 #[arg(help = "Deployment job ID")]
2417 pub job_id: String,
2418 #[arg(long, help = "Deployment job status enum value")]
2419 pub status: String,
2420 #[arg(long, help = "Optional error text")]
2421 pub error_text: Option<String>,
2422 #[command(flatten)]
2423 pub target: ApiTargetOptions,
2424}
2425
2426#[derive(Args, Debug)]
2427pub struct ApiRoutesCmd {
2428 #[command(subcommand)]
2429 pub command: ApiRoutesSubCommand,
2430}
2431
2432#[derive(Subcommand, Debug)]
2433pub enum ApiRoutesSubCommand {
2434 #[command(about = "List configured proxy routes")]
2435 List(ApiRoutesListCmd),
2436 #[command(about = "Create or replace a proxy route")]
2437 Create(ApiRoutesCreateCmd),
2438 #[command(about = "Delete a proxy route by domain")]
2439 Delete(ApiRoutesDeleteCmd),
2440}
2441
2442#[derive(Args, Debug)]
2443pub struct ApiRoutesListCmd {
2444 #[command(flatten)]
2445 pub target: ApiTargetOptions,
2446}
2447
2448#[derive(Args, Debug)]
2449pub struct ApiRoutesCreateCmd {
2450 #[arg(long, help = "Domain name for the route")]
2451 pub domain: String,
2452 #[arg(long, help = "Upstream target URL", required = true)]
2453 pub target: Vec<String>,
2454 #[arg(
2455 long,
2456 help = "Weighted upstream target in url=weight form",
2457 value_name = "URL=WEIGHT"
2458 )]
2459 pub weighted_target: Vec<String>,
2460 #[arg(long, help = "Optional header condition")]
2461 pub header_condition: Option<String>,
2462 #[arg(long, help = "Optional path prefix condition")]
2463 pub path_prefix: Option<String>,
2464 #[command(flatten)]
2465 pub target_options: ApiTargetOptions,
2466}
2467
2468#[derive(Args, Debug)]
2469pub struct ApiRoutesDeleteCmd {
2470 #[arg(help = "Domain name for the route")]
2471 pub domain: String,
2472 #[command(flatten)]
2473 pub target: ApiTargetOptions,
2474}
2475
2476#[derive(Args, Debug)]
2477pub struct ApiRequestCmd {
2478 #[arg(help = "Request path like /projects or a full https:// URL")]
2479 pub path: String,
2480 #[arg(
2481 short = 'X',
2482 long,
2483 help = "HTTP method to use (default: GET, or POST when a body is provided)"
2484 )]
2485 pub method: Option<String>,
2486 #[arg(short = 'd', long, help = "Inline request body string, typically JSON")]
2487 pub body: Option<String>,
2488 #[arg(long, help = "Read the request body from a file")]
2489 pub body_file: Option<PathBuf>,
2490 #[command(flatten)]
2491 pub target: ApiTargetOptions,
2492}
2493#[derive(Args, Debug)]
2494pub struct TailCmd {
2495 #[arg(long, help = "Tail Kafka topic instead of log files")]
2496 pub kafka: bool,
2497 #[arg(long, help = "Ship logs to Kafka")]
2498 pub ship: bool,
2499}
2500
2501#[derive(Args, Debug)]
2502#[command(
2503 arg_required_else_help = true,
2504 disable_help_subcommand = true,
2505 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2506 after_help = crate::cli::help_render::GENERATE_AFTER_HELP
2507)]
2508pub struct GenerateCmd {
2509 #[command(subcommand)]
2510 pub command: GenerateSubCommand,
2511}
2512
2513#[derive(Subcommand, Debug)]
2514pub enum GenerateSubCommand {
2515 #[command(about = "Generate or update .xbp/xbp.yaml (and convert legacy JSON)")]
2516 Config(GenerateConfigCmd),
2517 Systemd(GenerateSystemdCmd),
2518}
2519
2520#[derive(Args, Debug)]
2521pub struct GenerateConfigCmd {
2522 #[arg(
2523 long,
2524 help = "Overwrite .xbp/xbp.yaml if it already exists (default errors when present)"
2525 )]
2526 pub force: bool,
2527 #[arg(
2528 long,
2529 help = "Refresh .xbp/xbp.yaml by applying project detection defaults for missing fields"
2530 )]
2531 pub update: bool,
2532 #[arg(
2533 long,
2534 help = "Path to a legacy xbp.json file to convert into .xbp/xbp.yaml"
2535 )]
2536 pub from_json: Option<PathBuf>,
2537}
2538
2539#[derive(Args, Debug)]
2540#[command(
2541 arg_required_else_help = true,
2542 disable_help_subcommand = true,
2543 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
2544 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"
2545)]
2546pub struct WorkersCmd {
2547 #[arg(
2548 long,
2549 help = "Workers project root (defaults to current dir, or apps/web inside the current XBP repo when present)"
2550 )]
2551 pub root: Option<PathBuf>,
2552 #[arg(
2553 long,
2554 help = "Worker app name from .xbp/xbp.yaml `workers:` (defaults to the app matching the current directory)"
2555 )]
2556 pub app: Option<String>,
2557 #[arg(long, help = "Cloudflare API token override")]
2558 pub token: Option<String>,
2559 #[arg(long, help = "Cloudflare account ID override")]
2560 pub account_id: Option<String>,
2561 #[command(subcommand)]
2562 pub command: WorkersSubCommand,
2563}
2564
2565#[derive(Subcommand, Debug)]
2566pub enum WorkersSubCommand {
2567 #[command(
2568 alias = "ls",
2569 about = "List Cloudflare Worker scripts with build and placement status",
2570 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"
2571 )]
2572 List(WorkersListCmd),
2573 #[command(
2574 about = "Show deployment status, stream runtime logs, or fetch Workers Builds CI logs",
2575 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"
2576 )]
2577 Logs(WorkersLogsCmd),
2578 #[command(about = "Manage secret bindings for a Worker script or Wrangler environment")]
2579 Secrets(WorkersSecretsCmd),
2580 #[command(about = "Fetch remote Worker settings via the Cloudflare API")]
2581 Settings(WorkersSettingsCmd),
2582 #[command(about = "Inspect or generate Wrangler config helpers")]
2583 Wrangler(WorkersWranglerCmd),
2584 #[command(about = "Run Wrangler D1 migration helpers")]
2585 D1(WorkersD1Cmd),
2586 #[command(about = "Run Worker deploy and predeploy helpers")]
2587 Deploy(WorkersDeployCmd),
2588 #[command(about = "Inspect shared worktree paths or link shared dev files")]
2589 Worktree(WorkersWorktreeCmd),
2590 #[command(about = "Show resolved Worker runtime metadata from local env and config files")]
2591 Env(WorkersEnvCmd),
2592}
2593
2594#[derive(Args, Debug, Default)]
2595pub struct WorkersListCmd {
2596 #[arg(
2597 long,
2598 help = "Show every Worker in the account instead of only Workers for this XBP project"
2599 )]
2600 pub all: bool,
2601 #[arg(long, help = "Output the worker inventory as JSON")]
2602 pub json: bool,
2603 #[arg(short = 'n', long = "limit", help = "Show at most N workers")]
2604 pub limit: Option<usize>,
2605 #[arg(
2606 long = "status",
2607 help = "Filter by build status (failed, success, running, unknown)"
2608 )]
2609 pub status: Option<String>,
2610 #[arg(long, help = "Shortcut for --status failed")]
2611 pub failed: bool,
2612 #[arg(
2613 long = "sort",
2614 default_value = "name",
2615 help = "Sort workers by name, modified, or status"
2616 )]
2617 pub sort: String,
2618 #[arg(long = "no-color", help = "Disable ANSI colors in table output")]
2619 pub no_color: bool,
2620}
2621
2622#[derive(Args, Debug)]
2623pub struct WorkersLogsCmd {
2624 #[command(flatten)]
2625 pub target: WorkersTargetArgs,
2626 #[arg(
2627 short = 'f',
2628 long = "follow",
2629 help = "Stream runtime logs until interrupted (wrangler tail)"
2630 )]
2631 pub follow: bool,
2632 #[arg(
2633 long = "build",
2634 help = "Show Workers Builds CI logs instead of runtime deployment output"
2635 )]
2636 pub build: bool,
2637 #[arg(
2638 long = "failed",
2639 help = "When using --build, prefer the latest failed build"
2640 )]
2641 pub failed: bool,
2642 #[arg(long, help = "Output logs as JSON")]
2643 pub json: bool,
2644 #[arg(
2645 short = 'n',
2646 long = "lines",
2647 help = "Show only the last N log lines (deployments/build output)"
2648 )]
2649 pub lines: Option<usize>,
2650 #[arg(
2651 short = 'o',
2652 long = "output",
2653 help = "Write log output to a file instead of stdout"
2654 )]
2655 pub output: Option<PathBuf>,
2656 #[arg(
2657 short = 'g',
2658 long = "grep",
2659 help = "Filter log lines matching this pattern (case-insensitive substring)"
2660 )]
2661 pub grep: Option<String>,
2662 #[arg(
2663 short = 'e',
2664 long = "errors-only",
2665 help = "Show only lines that look like errors or failures"
2666 )]
2667 pub errors_only: bool,
2668 #[arg(long = "no-color", help = "Disable ANSI colors in log output")]
2669 pub no_color: bool,
2670 #[arg(
2671 long = "list-builds",
2672 help = "List recent Workers Builds before fetching logs (with --build)"
2673 )]
2674 pub list_builds: bool,
2675 #[arg(
2676 long = "build-index",
2677 help = "Select build by index from --list-builds (0 = latest)"
2678 )]
2679 pub build_index: Option<usize>,
2680 #[arg(
2681 long,
2682 help = "Poll until the selected Workers Build finishes before fetching logs"
2683 )]
2684 pub wait: bool,
2685 #[arg(long = "no-wait", help = "Skip polling for in-progress Workers Builds")]
2686 pub no_wait: bool,
2687 #[arg(
2688 long = "wait-seconds",
2689 default_value_t = 120,
2690 help = "Maximum seconds to poll an in-progress Workers Build"
2691 )]
2692 pub wait_seconds: u64,
2693 #[arg(help = "Worker script name. When omitted, xbp prompts interactively.")]
2694 pub script_name: Option<String>,
2695}
2696
2697#[derive(Args, Debug, Clone, Default)]
2698pub struct WorkersTargetArgs {
2699 #[arg(
2700 long,
2701 help = "Worker base name (defaults to wrangler config name or xbp)"
2702 )]
2703 pub worker: Option<String>,
2704 #[arg(
2705 long = "environment",
2706 alias = "env",
2707 help = "Wrangler environment name. The remote script resolves to <worker>-<environment>."
2708 )]
2709 pub environment: Option<String>,
2710 #[arg(
2711 long,
2712 help = "Exact remote script name override. When set, this bypasses <worker>-<environment> resolution."
2713 )]
2714 pub script: Option<String>,
2715}
2716
2717#[derive(Args, Debug)]
2718pub struct WorkersSecretsCmd {
2719 #[command(flatten)]
2720 pub target: WorkersTargetArgs,
2721 #[command(subcommand)]
2722 pub command: WorkersSecretsSubCommand,
2723}
2724
2725#[derive(Subcommand, Debug)]
2726pub enum WorkersSecretsSubCommand {
2727 #[command(about = "List secret bindings on the resolved Worker script")]
2728 List(WorkersSecretsListCmd),
2729 #[command(about = "Fetch one secret binding metadata or value")]
2730 Get(WorkersSecretsGetCmd),
2731 #[command(about = "Create or update a secret binding")]
2732 Put(WorkersSecretsPutCmd),
2733 #[command(about = "Delete a secret binding")]
2734 Delete(WorkersSecretsDeleteCmd),
2735 #[command(about = "Create, update, or delete multiple secret bindings from a file")]
2736 Bulk(WorkersSecretsBulkCmd),
2737}
2738
2739#[derive(Args, Debug, Default)]
2740pub struct WorkersSecretsListCmd {}
2741
2742#[derive(Args, Debug)]
2743pub struct WorkersSecretsGetCmd {
2744 #[arg(long, help = "Secret binding name")]
2745 pub name: String,
2746}
2747
2748#[derive(Args, Debug)]
2749pub struct WorkersSecretsPutCmd {
2750 #[arg(long, help = "Secret binding name")]
2751 pub name: String,
2752 #[arg(long, help = "Secret value")]
2753 pub value: Option<String>,
2754 #[arg(long, help = "Read the secret value from stdin instead of --value")]
2755 pub from_stdin: bool,
2756}
2757
2758#[derive(Args, Debug)]
2759pub struct WorkersSecretsDeleteCmd {
2760 #[arg(long, help = "Secret binding name")]
2761 pub name: String,
2762}
2763
2764#[derive(Args, Debug)]
2765pub struct WorkersSecretsBulkCmd {
2766 #[arg(long, help = "Path to a .env or JSON file containing secret updates")]
2767 pub file: PathBuf,
2768 #[arg(
2769 long,
2770 default_value = "env",
2771 help = "Input format: env or json. For json, pass an object mapping names to string values or null for deletes."
2772 )]
2773 pub format: String,
2774}
2775
2776#[derive(Args, Debug)]
2777pub struct WorkersSettingsCmd {
2778 #[command(flatten)]
2779 pub target: WorkersTargetArgs,
2780}
2781
2782#[derive(Args, Debug)]
2783pub struct WorkersWranglerCmd {
2784 #[command(subcommand)]
2785 pub command: WorkersWranglerSubCommand,
2786}
2787
2788#[derive(Subcommand, Debug)]
2789pub enum WorkersWranglerSubCommand {
2790 #[command(about = "Generate a Wrangler deploy config JSON file from env vars")]
2791 GenerateConfig(WorkersWranglerGenerateConfigCmd),
2792 #[command(about = "Resolve which Wrangler config file local dev should use")]
2793 ConfigPath(WorkersWranglerConfigPathCmd),
2794}
2795
2796#[derive(Args, Debug)]
2797pub struct WorkersWranglerGenerateConfigCmd {
2798 #[arg(
2799 long,
2800 default_value = "wrangler.deploy.json",
2801 help = "Output filename, relative to the worker root unless absolute"
2802 )]
2803 pub output: PathBuf,
2804}
2805
2806#[derive(Args, Debug)]
2807pub struct WorkersWranglerConfigPathCmd {
2808 #[arg(
2809 long,
2810 default_value = "serve",
2811 help = "Calling command name, for example serve"
2812 )]
2813 pub command_name: String,
2814 #[arg(
2815 long,
2816 default_value = "development",
2817 help = "Execution mode, for example development or production"
2818 )]
2819 pub mode: String,
2820}
2821
2822#[derive(Args, Debug)]
2823pub struct WorkersD1Cmd {
2824 #[command(subcommand)]
2825 pub command: WorkersD1SubCommand,
2826}
2827
2828#[derive(Subcommand, Debug)]
2829pub enum WorkersD1SubCommand {
2830 #[command(about = "Apply pending Wrangler D1 migrations")]
2831 Migrations(WorkersD1MigrationsCmd),
2832}
2833
2834#[derive(Args, Debug)]
2835pub struct WorkersD1MigrationsCmd {
2836 #[command(subcommand)]
2837 pub command: WorkersD1MigrationsSubCommand,
2838}
2839
2840#[derive(Subcommand, Debug)]
2841pub enum WorkersD1MigrationsSubCommand {
2842 #[command(about = "Apply pending migrations to a local or remote D1 database")]
2843 Apply(WorkersD1MigrationsApplyCmd),
2844}
2845
2846#[derive(Args, Debug)]
2847pub struct WorkersD1MigrationsApplyCmd {
2848 #[arg(help = "D1 database binding or name, for example DB")]
2849 pub database: String,
2850 #[arg(
2851 long,
2852 conflicts_with = "remote",
2853 help = "Apply migrations to the local Wrangler D1 database"
2854 )]
2855 pub local: bool,
2856 #[arg(
2857 long,
2858 conflicts_with = "local",
2859 help = "Apply migrations to the remote D1 database"
2860 )]
2861 pub remote: bool,
2862 #[arg(long, help = "Wrangler config path override")]
2863 pub config: Option<PathBuf>,
2864 #[command(flatten)]
2865 pub target: WorkersTargetArgs,
2866 #[arg(
2867 long,
2868 help = "Persist local D1 state to this directory. When omitted in a git worktree, xbp uses the shared .wrangler/state path automatically."
2869 )]
2870 pub persist_to: Option<PathBuf>,
2871 #[arg(
2872 long,
2873 help = "Disable the automatic shared .wrangler/state path when running local migrations from a git worktree"
2874 )]
2875 pub no_shared_worktree_state: bool,
2876}
2877
2878#[derive(Args, Debug)]
2879pub struct WorkersDeployCmd {
2880 #[arg(
2881 long,
2882 help = "Run the deploy action for every worker app configured in .xbp/xbp.yaml"
2883 )]
2884 pub all: bool,
2885 #[command(subcommand)]
2886 pub command: WorkersDeploySubCommand,
2887}
2888
2889#[derive(Subcommand, Debug)]
2890pub enum WorkersDeploySubCommand {
2891 #[command(
2892 about = "Discover worker apps, sync Wrangler config, and optionally write .xbp/xbp.yaml"
2893 )]
2894 Configure(WorkersDeployConfigureCmd),
2895 #[command(about = "Run the configured deploy command for one or more worker apps")]
2896 Run(WorkersDeployRunCmd),
2897 #[command(about = "Run the predeploy sync flow unless Workers CI mode is active")]
2898 Predeploy(WorkersDeployPredeployCmd),
2899 #[command(about = "Read .env.local or process env, then emit .dev.vars and Wrangler configs")]
2900 SyncEnvLocal(WorkersDeploySyncEnvLocalCmd),
2901 #[command(
2902 about = "Run the built-in Cloudflare Worker CI deploy workflow (build + wrangler deploy)"
2903 )]
2904 Ci(WorkersDeployCiCmd),
2905 #[command(
2906 about = "Run the existing deploy-selection flow that chooses CI or local deploy behavior"
2907 )]
2908 Select(WorkersDeploySelectCmd),
2909}
2910
2911#[derive(Args, Debug, Default)]
2912pub struct WorkersDeployConfigureCmd {
2913 #[arg(
2914 long,
2915 help = "Scan the repo for Wrangler projects and merge them into .xbp/xbp.yaml `workers:`"
2916 )]
2917 pub write_config: bool,
2918 #[arg(
2919 long,
2920 help = "Preview worker discovery and config writes without changing files"
2921 )]
2922 pub dry_run: bool,
2923}
2924
2925#[derive(Args, Debug, Default)]
2926pub struct WorkersDeployRunCmd {}
2927
2928#[derive(Args, Debug)]
2929pub struct WorkersDeployPredeployCmd {
2930 #[arg(long, help = "Force Workers CI mode and skip local sync")]
2931 pub ci: bool,
2932}
2933
2934#[derive(Args, Debug)]
2935pub struct WorkersDeploySyncEnvLocalCmd {}
2936
2937#[derive(Args, Debug)]
2938pub struct WorkersDeployCiCmd {
2939 #[arg(long, help = "Upload a new version without immediately deploying it")]
2940 pub version_upload: bool,
2941}
2942
2943#[derive(Args, Debug)]
2944pub struct WorkersDeploySelectCmd {
2945 #[arg(long, help = "Force the WORKERS_CI=1 branch of the selector")]
2946 pub ci: bool,
2947 #[arg(
2948 long,
2949 help = "Branch name to expose as WORKERS_CI_BRANCH when --ci is set"
2950 )]
2951 pub branch: Option<String>,
2952}
2953
2954#[derive(Args, Debug)]
2955pub struct WorkersWorktreeCmd {
2956 #[command(subcommand)]
2957 pub command: WorkersWorktreeSubCommand,
2958}
2959
2960#[derive(Subcommand, Debug)]
2961pub enum WorkersWorktreeSubCommand {
2962 #[command(about = "Print repo-root, primary worktree, and shared Wrangler state paths")]
2963 Paths(WorkersWorktreePathsCmd),
2964 #[command(
2965 about = "Symlink apps/web/.dev.vars and wrangler.dev.jsonc from the primary worktree when in a linked worktree"
2966 )]
2967 LinkDevVars(WorkersWorktreeLinkDevVarsCmd),
2968}
2969
2970#[derive(Args, Debug, Default)]
2971pub struct WorkersWorktreePathsCmd {}
2972
2973#[derive(Args, Debug, Default)]
2974pub struct WorkersWorktreeLinkDevVarsCmd {}
2975
2976#[derive(Args, Debug)]
2977pub struct WorkersEnvCmd {
2978 #[command(flatten)]
2979 pub target: WorkersTargetArgs,
2980 #[arg(
2981 long,
2982 help = "Show resolved plain-text binding values instead of masking them"
2983 )]
2984 pub show_values: bool,
2985}
2986
2987#[cfg(feature = "secrets")]
2988#[derive(Args, Debug)]
2989pub struct SecretsCmd {
2990 #[arg(long, value_enum, default_value_t = SecretsProviderKind::Github, help = "Secrets provider to use")]
2991 pub provider: SecretsProviderKind,
2992 #[arg(long, help = "GitHub repository override (owner/repo)")]
2993 pub repo: Option<String>,
2994 #[arg(
2995 long,
2996 help = "Provider token override (GitHub token or Cloudflare API token)"
2997 )]
2998 pub token: Option<String>,
2999 #[arg(long, help = "Cloudflare account ID override")]
3000 pub account_id: Option<String>,
3001 #[arg(
3002 long = "environment",
3003 alias = "env",
3004 default_value = "xbp-dev",
3005 help = "Environment to sync (default: xbp-dev). Nested services are scoped automatically, e.g. xbp-dev-web."
3006 )]
3007 pub environment: String,
3008 #[arg(
3009 long,
3010 help = "Service name from .xbp/xbp.yaml. If omitted, XBP resolves it from the current directory or prompts when ambiguous."
3011 )]
3012 pub service: Option<String>,
3013 #[command(subcommand)]
3014 pub command: Option<SecretsSubCommand>,
3015}
3016
3017#[cfg(feature = "secrets")]
3018#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
3019pub enum SecretsProviderKind {
3020 Github,
3021 Cloudflare,
3022 Railway,
3023 Vercel,
3024}
3025
3026#[cfg(feature = "secrets")]
3027#[derive(Subcommand, Debug)]
3028pub enum SecretsSubCommand {
3029 #[command(alias = "ls", alias = "list-providers")]
3031 Providers,
3032 List(ListCmd),
3034 Push(PushCmd),
3036 Pull(PullCmd),
3038 GenerateDefault(GenerateDefaultCmd),
3040 GenerateExample(GenerateExampleCmd),
3042 Diff,
3044 Verify,
3046 #[command(name = "diag", alias = "doctor")]
3048 Diag,
3049 Stores(SecretsStoresCmd),
3051 Secrets(CloudflareSecretsCmd),
3053 Quota(SecretsQuotaCmd),
3055 #[command(name = "usage")]
3057 Usage,
3058}
3059
3060#[cfg(feature = "secrets")]
3061#[derive(Args, Debug)]
3062pub struct ListCmd {
3063 #[arg(long, help = "Env file to list (.env.local, .env, .env.default)")]
3064 pub file: Option<String>,
3065 #[arg(long, help = "Output format: plain (default) or json")]
3066 pub format: Option<String>,
3067}
3068
3069#[cfg(feature = "secrets")]
3070#[derive(Args, Debug)]
3071pub struct PushCmd {
3072 #[arg(long, help = "Path to env file (default: .env.local/.env)")]
3073 pub file: Option<String>,
3074 #[arg(
3075 long,
3076 help = "Force overwrite existing GitHub Actions environment variables"
3077 )]
3078 pub force: bool,
3079 #[arg(long, help = "Show what would be pushed without making changes")]
3080 pub dry_run: bool,
3081}
3082
3083#[cfg(feature = "secrets")]
3084#[derive(Args, Debug)]
3085pub struct PullCmd {
3086 #[arg(long, help = "Output file path (default: .env.local)")]
3087 pub output: Option<String>,
3088}
3089
3090#[cfg(feature = "secrets")]
3091#[derive(Args, Debug)]
3092pub struct GenerateDefaultCmd {
3093 #[arg(long, help = "Output file path (default: .env.default)")]
3094 pub output: Option<String>,
3095}
3096
3097#[cfg(feature = "secrets")]
3098#[derive(Args, Debug)]
3099pub struct GenerateExampleCmd {
3100 #[arg(long, help = "Output file path (default: .env.example)")]
3101 pub output: Option<String>,
3102 #[arg(long, help = "Remove keys from .env.local not in .env.example")]
3103 pub clean: bool,
3104 #[arg(long, help = "Only include vars matching prefix (repeatable)")]
3105 pub include_prefix: Vec<String>,
3106 #[arg(long, help = "Exclude vars matching prefix (repeatable)")]
3107 pub exclude_prefix: Vec<String>,
3108}
3109
3110#[cfg(feature = "secrets")]
3111#[derive(Args, Debug)]
3112pub struct SecretsStoresCmd {
3113 #[command(subcommand)]
3114 pub command: SecretsStoresSubCommand,
3115}
3116
3117#[cfg(feature = "secrets")]
3118#[derive(Subcommand, Debug)]
3119pub enum SecretsStoresSubCommand {
3120 List(CloudflareSecretsStoreListCmd),
3121 Get(CloudflareSecretsStoreGetCmd),
3122 Create(CloudflareSecretsStoreCreateCmd),
3123 Delete(CloudflareSecretsStoreDeleteCmd),
3124}
3125
3126#[cfg(feature = "secrets")]
3127#[derive(Args, Debug)]
3128pub struct CloudflareSecretsStoreListCmd {}
3129
3130#[cfg(feature = "secrets")]
3131#[derive(Args, Debug)]
3132pub struct CloudflareSecretsStoreGetCmd {
3133 #[arg(long)]
3134 pub store_id: String,
3135}
3136
3137#[cfg(feature = "secrets")]
3138#[derive(Args, Debug)]
3139pub struct CloudflareSecretsStoreCreateCmd {
3140 #[arg(long)]
3141 pub name: String,
3142}
3143
3144#[cfg(feature = "secrets")]
3145#[derive(Args, Debug)]
3146pub struct CloudflareSecretsStoreDeleteCmd {
3147 #[arg(long)]
3148 pub store_id: String,
3149}
3150
3151#[cfg(feature = "secrets")]
3152#[derive(Args, Debug)]
3153pub struct CloudflareSecretsCmd {
3154 #[command(subcommand)]
3155 pub command: CloudflareSecretsSubCommand,
3156}
3157
3158#[cfg(feature = "secrets")]
3159#[derive(Subcommand, Debug)]
3160pub enum CloudflareSecretsSubCommand {
3161 List(CloudflareSecretsListCmd),
3162 Get(CloudflareSecretsGetCmd),
3163 Create(CloudflareSecretsCreateCmd),
3164 Edit(CloudflareSecretsEditCmd),
3165 Delete(CloudflareSecretsDeleteCmd),
3166 #[command(name = "delete-bulk")]
3167 DeleteBulk(CloudflareSecretsBulkDeleteCmd),
3168 Duplicate(CloudflareSecretsDuplicateCmd),
3169}
3170
3171#[cfg(feature = "secrets")]
3172#[derive(Args, Debug)]
3173pub struct CloudflareSecretsListCmd {
3174 #[arg(long)]
3175 pub store_id: String,
3176}
3177
3178#[cfg(feature = "secrets")]
3179#[derive(Args, Debug)]
3180pub struct CloudflareSecretsGetCmd {
3181 #[arg(long)]
3182 pub store_id: String,
3183 #[arg(long)]
3184 pub secret_id: String,
3185}
3186
3187#[cfg(feature = "secrets")]
3188#[derive(Args, Debug)]
3189pub struct CloudflareSecretsCreateCmd {
3190 #[arg(long)]
3191 pub store_id: String,
3192 #[arg(long)]
3193 pub name: String,
3194 #[arg(long)]
3195 pub value: String,
3196 #[arg(long, value_delimiter = ',')]
3197 pub scopes: Vec<String>,
3198 #[arg(long)]
3199 pub comment: Option<String>,
3200}
3201
3202#[cfg(feature = "secrets")]
3203#[derive(Args, Debug)]
3204pub struct CloudflareSecretsEditCmd {
3205 #[arg(long)]
3206 pub store_id: String,
3207 #[arg(long)]
3208 pub secret_id: String,
3209 #[arg(long)]
3210 pub name: Option<String>,
3211 #[arg(long)]
3212 pub value: Option<String>,
3213 #[arg(long, value_delimiter = ',')]
3214 pub scopes: Vec<String>,
3215 #[arg(long)]
3216 pub comment: Option<String>,
3217}
3218
3219#[cfg(feature = "secrets")]
3220#[derive(Args, Debug)]
3221pub struct CloudflareSecretsDeleteCmd {
3222 #[arg(long)]
3223 pub store_id: String,
3224 #[arg(long)]
3225 pub secret_id: String,
3226}
3227
3228#[cfg(feature = "secrets")]
3229#[derive(Args, Debug)]
3230pub struct CloudflareSecretsBulkDeleteCmd {
3231 #[arg(long)]
3232 pub store_id: String,
3233 #[arg(long = "secret-id", required = true)]
3234 pub secret_ids: Vec<String>,
3235}
3236
3237#[cfg(feature = "secrets")]
3238#[derive(Args, Debug)]
3239pub struct CloudflareSecretsDuplicateCmd {
3240 #[arg(long)]
3241 pub store_id: String,
3242 #[arg(long)]
3243 pub secret_id: String,
3244 #[arg(long)]
3245 pub name: String,
3246 #[arg(long, value_delimiter = ',')]
3247 pub scopes: Vec<String>,
3248 #[arg(long)]
3249 pub comment: Option<String>,
3250}
3251
3252#[cfg(feature = "secrets")]
3253#[derive(Args, Debug)]
3254pub struct SecretsQuotaCmd {
3255 #[command(subcommand)]
3256 pub command: SecretsQuotaSubCommand,
3257}
3258
3259#[cfg(feature = "secrets")]
3260#[derive(Subcommand, Debug)]
3261pub enum SecretsQuotaSubCommand {
3262 Get(SecretsQuotaGetCmd),
3263}
3264
3265#[cfg(feature = "secrets")]
3266#[derive(Args, Debug)]
3267pub struct SecretsQuotaGetCmd {}
3268
3269const DNS_HELP_TEMPLATE: &str = crate::cli::help_render::XBP_HELP_TEMPLATE;
3270
3271const DNS_COMMAND_AFTER_HELP: &str = "\
3272Examples:
3273 xbp dns providers
3274 xbp dns zones list --provider cloudflare --account-id acc_123
3275 xbp dns records list --provider cloudflare --zone-id zone_123
3276 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
3277 xbp dns dnssec get --provider cloudflare --zone-id zone_123
3278 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
3279
3280Notes:
3281 Start with `xbp dns providers` to see what is implemented today.
3282 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`).";
3283
3284const DNS_ZONES_AFTER_HELP: &str = "\
3285Examples:
3286 xbp dns zones list --provider cloudflare --account-id acc_123
3287 xbp dns zones get --provider cloudflare --zone-id zone_123
3288 xbp dns zones create --provider cloudflare --name example.com --account-id acc_123 --jump-start
3289 xbp dns zones edit --provider cloudflare --zone-id zone_123 --paused true
3290 xbp dns zones delete --provider cloudflare --zone-id zone_123";
3291
3292const DNS_RECORDS_AFTER_HELP: &str = "\
3293Examples:
3294 xbp dns records list --provider cloudflare --zone-id zone_123
3295 xbp dns records get --provider cloudflare --zone-id zone_123 --record-id rec_123
3296 xbp dns records create --provider cloudflare --zone-id zone_123 --type A --name api --content 127.0.0.1
3297 xbp dns records edit --provider cloudflare --zone-id zone_123 --record-id rec_123 --proxied true
3298 xbp dns records import --provider cloudflare --zone-id zone_123 --file zone.txt
3299 xbp dns records export --provider cloudflare --zone-id zone_123 --output zone.txt";
3300
3301const DNS_DNSSEC_AFTER_HELP: &str = "\
3302Examples:
3303 xbp dns dnssec get --provider cloudflare --zone-id zone_123
3304 xbp dns dnssec edit --provider cloudflare --zone-id zone_123 --status active";
3305
3306const DNS_SETTINGS_AFTER_HELP: &str = "\
3307Examples:
3308 xbp dns settings get --provider cloudflare --zone-id zone_123
3309 xbp dns settings edit --provider cloudflare --zone-id zone_123 --flatten-all-cnames true
3310 xbp dns settings edit --provider cloudflare --zone-id zone_123 --nameservers-type custom --nameservers-ns-set 2";
3311
3312const DNS_PROVIDERS_AFTER_HELP: &str = "\
3313Examples:
3314 xbp dns providers
3315
3316What this shows:
3317 Implemented providers are wired into `xbp dns` today.
3318 Planned providers are tracked in the CLI surface but not callable yet.";
3319
3320#[derive(Args, Debug)]
3321#[command(
3322 about = "Manage DNS providers, zones, records, DNSSEC, and provider-level settings",
3323 arg_required_else_help = true,
3324 disable_help_subcommand = true,
3325 help_template = DNS_HELP_TEMPLATE,
3326 after_help = DNS_COMMAND_AFTER_HELP
3327)]
3328pub struct DnsCmd {
3329 #[command(subcommand)]
3330 pub command: DnsSubCommand,
3331}
3332
3333#[derive(Subcommand, Debug)]
3334pub enum DnsSubCommand {
3335 #[command(
3336 alias = "ls",
3337 alias = "list",
3338 about = "List supported DNS providers and current implementation status",
3339 after_help = DNS_PROVIDERS_AFTER_HELP
3340 )]
3341 Providers,
3342 #[command(about = "Inspect and manage DNS zones")]
3343 Zones(DnsZonesCmd),
3344 #[command(about = "List, create, edit, import, export, and batch DNS records")]
3345 Records(DnsRecordsCmd),
3346 #[command(about = "Inspect or edit DNSSEC status for a zone")]
3347 Dnssec(DnssecCmd),
3348 #[command(about = "Inspect or edit provider DNS settings for a zone")]
3349 Settings(DnsSettingsCmd),
3350}
3351
3352#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
3353pub enum DnsProviderKind {
3354 Cloudflare,
3355 Hetzner,
3356 Vercel,
3357 Custom,
3358}
3359
3360#[derive(Args, Debug)]
3361#[command(
3362 about = "Inspect and manage DNS zones",
3363 arg_required_else_help = true,
3364 help_template = DNS_HELP_TEMPLATE,
3365 after_help = DNS_ZONES_AFTER_HELP
3366)]
3367pub struct DnsZonesCmd {
3368 #[command(subcommand)]
3369 pub command: DnsZonesSubCommand,
3370}
3371
3372#[derive(Subcommand, Debug)]
3373pub enum DnsZonesSubCommand {
3374 #[command(about = "List zones for a provider account")]
3375 List(DnsZoneListCmd),
3376 #[command(about = "Fetch one zone by id")]
3377 Get(DnsZoneGetCmd),
3378 #[command(about = "Create a new zone")]
3379 Create(DnsZoneCreateCmd),
3380 #[command(about = "Edit zone-level properties")]
3381 Edit(DnsZoneEditCmd),
3382 #[command(about = "Delete a zone")]
3383 Delete(DnsZoneDeleteCmd),
3384}
3385
3386#[derive(Args, Debug)]
3387pub struct DnsZoneListCmd {
3388 #[arg(long, value_enum, default_value = "cloudflare")]
3389 pub provider: DnsProviderKind,
3390 #[arg(long)]
3391 pub account_id: Option<String>,
3392 #[arg(long)]
3393 pub account_name: Option<String>,
3394 #[arg(long = "account-name-op")]
3395 pub account_name_op: Option<String>,
3396 #[arg(long)]
3397 pub name: Option<String>,
3398 #[arg(long = "name-op")]
3399 pub name_op: Option<String>,
3400 #[arg(long)]
3401 pub status: Option<String>,
3402 #[arg(long = "type", value_delimiter = ',')]
3403 pub zone_types: Vec<String>,
3404 #[arg(long)]
3405 pub r#match: Option<String>,
3406 #[arg(long)]
3407 pub order: Option<String>,
3408 #[arg(long)]
3409 pub direction: Option<String>,
3410 #[arg(long)]
3411 pub page: Option<u64>,
3412 #[arg(long = "per-page")]
3413 pub per_page: Option<u64>,
3414 #[arg(long)]
3415 pub token: Option<String>,
3416}
3417
3418#[derive(Args, Debug)]
3419pub struct DnsZoneGetCmd {
3420 #[arg(long, value_enum, default_value = "cloudflare")]
3421 pub provider: DnsProviderKind,
3422 #[arg(long)]
3423 pub zone_id: String,
3424 #[arg(long)]
3425 pub token: Option<String>,
3426}
3427
3428#[derive(Args, Debug)]
3429pub struct DnsZoneCreateCmd {
3430 #[arg(long, value_enum, default_value = "cloudflare")]
3431 pub provider: DnsProviderKind,
3432 #[arg(long)]
3433 pub name: String,
3434 #[arg(long)]
3435 pub account_id: Option<String>,
3436 #[arg(long)]
3437 pub jump_start: bool,
3438 #[arg(long = "type")]
3439 pub zone_type: Option<String>,
3440 #[arg(long)]
3441 pub token: Option<String>,
3442}
3443
3444#[derive(Args, Debug)]
3445pub struct DnsZoneEditCmd {
3446 #[arg(long, value_enum, default_value = "cloudflare")]
3447 pub provider: DnsProviderKind,
3448 #[arg(long)]
3449 pub zone_id: String,
3450 #[arg(long)]
3451 pub paused: Option<bool>,
3452 #[arg(long = "type")]
3453 pub zone_type: Option<String>,
3454 #[arg(long = "vanity-name-server")]
3455 pub vanity_name_servers: Vec<String>,
3456 #[arg(long)]
3457 pub token: Option<String>,
3458}
3459
3460#[derive(Args, Debug)]
3461pub struct DnsZoneDeleteCmd {
3462 #[arg(long, value_enum, default_value = "cloudflare")]
3463 pub provider: DnsProviderKind,
3464 #[arg(long)]
3465 pub zone_id: String,
3466 #[arg(long)]
3467 pub token: Option<String>,
3468}
3469
3470#[derive(Args, Debug)]
3471#[command(
3472 about = "List, create, edit, import, export, and batch DNS records",
3473 arg_required_else_help = true,
3474 help_template = DNS_HELP_TEMPLATE,
3475 after_help = DNS_RECORDS_AFTER_HELP
3476)]
3477pub struct DnsRecordsCmd {
3478 #[command(subcommand)]
3479 pub command: DnsRecordsSubCommand,
3480}
3481
3482#[derive(Subcommand, Debug)]
3483pub enum DnsRecordsSubCommand {
3484 #[command(about = "List records in a zone")]
3485 List(DnsRecordListCmd),
3486 #[command(about = "Fetch one record by id")]
3487 Get(DnsRecordGetCmd),
3488 #[command(about = "Create a new DNS record")]
3489 Create(DnsRecordCreateCmd),
3490 #[command(about = "Replace a record by id with a full payload")]
3491 Replace(DnsRecordReplaceCmd),
3492 #[command(about = "Patch selected fields on a record")]
3493 Edit(DnsRecordEditCmd),
3494 #[command(about = "Delete a record")]
3495 Delete(DnsRecordDeleteCmd),
3496 #[command(about = "Apply a Cloudflare batch record payload from JSON")]
3497 Batch(DnsRecordBatchCmd),
3498 #[command(about = "Import a BIND-style zone file into a zone")]
3499 Import(DnsRecordImportCmd),
3500 #[command(about = "Export a zone as a BIND-style zone file")]
3501 Export(DnsRecordExportCmd),
3502}
3503
3504#[derive(Args, Debug)]
3505pub struct DnsRecordListCmd {
3506 #[arg(long, value_enum, default_value = "cloudflare")]
3507 pub provider: DnsProviderKind,
3508 #[arg(long)]
3509 pub zone_id: String,
3510 #[arg(long = "type")]
3511 pub record_type: Option<String>,
3512 #[arg(long)]
3513 pub name: Option<String>,
3514 #[arg(long)]
3515 pub page: Option<u64>,
3516 #[arg(long = "per-page")]
3517 pub per_page: Option<u64>,
3518 #[arg(long)]
3519 pub token: Option<String>,
3520}
3521
3522#[derive(Args, Debug)]
3523pub struct DnsRecordGetCmd {
3524 #[arg(long, value_enum, default_value = "cloudflare")]
3525 pub provider: DnsProviderKind,
3526 #[arg(long)]
3527 pub zone_id: String,
3528 #[arg(long)]
3529 pub record_id: String,
3530 #[arg(long)]
3531 pub token: Option<String>,
3532}
3533
3534#[derive(Args, Debug)]
3535pub struct DnsRecordCreateCmd {
3536 #[arg(long, value_enum, default_value = "cloudflare")]
3537 pub provider: DnsProviderKind,
3538 #[arg(long)]
3539 pub zone_id: String,
3540 #[arg(long = "type")]
3541 pub record_type: String,
3542 #[arg(long)]
3543 pub name: String,
3544 #[arg(long)]
3545 pub content: String,
3546 #[arg(long)]
3547 pub ttl: Option<u32>,
3548 #[arg(long)]
3549 pub proxied: Option<bool>,
3550 #[arg(long)]
3551 pub priority: Option<u32>,
3552 #[arg(long)]
3553 pub comment: Option<String>,
3554 #[arg(long = "tag")]
3555 pub tags: Vec<String>,
3556 #[arg(long = "data-json")]
3557 pub data_json: Option<String>,
3558 #[arg(long = "settings-json")]
3559 pub settings_json: Option<String>,
3560 #[arg(long)]
3561 pub token: Option<String>,
3562}
3563
3564#[derive(Args, Debug)]
3565pub struct DnsRecordReplaceCmd {
3566 #[command(flatten)]
3567 pub common: DnsRecordCreateCmd,
3568 #[arg(long)]
3569 pub record_id: String,
3570}
3571
3572#[derive(Args, Debug)]
3573pub struct DnsRecordEditCmd {
3574 #[arg(long, value_enum, default_value = "cloudflare")]
3575 pub provider: DnsProviderKind,
3576 #[arg(long)]
3577 pub zone_id: String,
3578 #[arg(long)]
3579 pub record_id: String,
3580 #[arg(long = "type")]
3581 pub record_type: Option<String>,
3582 #[arg(long)]
3583 pub name: Option<String>,
3584 #[arg(long)]
3585 pub content: Option<String>,
3586 #[arg(long)]
3587 pub ttl: Option<u32>,
3588 #[arg(long)]
3589 pub proxied: Option<bool>,
3590 #[arg(long)]
3591 pub priority: Option<u32>,
3592 #[arg(long)]
3593 pub comment: Option<String>,
3594 #[arg(long = "tag")]
3595 pub tags: Vec<String>,
3596 #[arg(long = "data-json")]
3597 pub data_json: Option<String>,
3598 #[arg(long = "settings-json")]
3599 pub settings_json: Option<String>,
3600 #[arg(long)]
3601 pub token: Option<String>,
3602}
3603
3604#[derive(Args, Debug)]
3605pub struct DnsRecordDeleteCmd {
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 record_id: String,
3612 #[arg(long)]
3613 pub token: Option<String>,
3614}
3615
3616#[derive(Args, Debug)]
3617pub struct DnsRecordBatchCmd {
3618 #[arg(long, value_enum, default_value = "cloudflare")]
3619 pub provider: DnsProviderKind,
3620 #[arg(long)]
3621 pub zone_id: String,
3622 #[arg(long)]
3623 pub input: PathBuf,
3624 #[arg(long)]
3625 pub token: Option<String>,
3626}
3627
3628#[derive(Args, Debug)]
3629pub struct DnsRecordImportCmd {
3630 #[arg(long, value_enum, default_value = "cloudflare")]
3631 pub provider: DnsProviderKind,
3632 #[arg(long)]
3633 pub zone_id: String,
3634 #[arg(long)]
3635 pub file: PathBuf,
3636 #[arg(long)]
3637 pub token: Option<String>,
3638}
3639
3640#[derive(Args, Debug)]
3641pub struct DnsRecordExportCmd {
3642 #[arg(long, value_enum, default_value = "cloudflare")]
3643 pub provider: DnsProviderKind,
3644 #[arg(long)]
3645 pub zone_id: String,
3646 #[arg(long)]
3647 pub output: Option<PathBuf>,
3648 #[arg(long)]
3649 pub token: Option<String>,
3650}
3651
3652#[derive(Args, Debug)]
3653#[command(
3654 about = "Inspect or edit DNSSEC status for a zone",
3655 arg_required_else_help = true,
3656 help_template = DNS_HELP_TEMPLATE,
3657 after_help = DNS_DNSSEC_AFTER_HELP
3658)]
3659pub struct DnssecCmd {
3660 #[command(subcommand)]
3661 pub command: DnssecSubCommand,
3662}
3663
3664#[derive(Subcommand, Debug)]
3665pub enum DnssecSubCommand {
3666 #[command(about = "Fetch DNSSEC state for a zone")]
3667 Get(DnssecGetCmd),
3668 #[command(about = "Edit DNSSEC-related flags for a zone")]
3669 Edit(DnssecEditCmd),
3670}
3671
3672#[derive(Args, Debug)]
3673pub struct DnssecGetCmd {
3674 #[arg(long, value_enum, default_value = "cloudflare")]
3675 pub provider: DnsProviderKind,
3676 #[arg(long)]
3677 pub zone_id: String,
3678 #[arg(long)]
3679 pub token: Option<String>,
3680}
3681
3682#[derive(Args, Debug)]
3683pub struct DnssecEditCmd {
3684 #[arg(long, value_enum, default_value = "cloudflare")]
3685 pub provider: DnsProviderKind,
3686 #[arg(long)]
3687 pub zone_id: String,
3688 #[arg(long)]
3689 pub status: Option<String>,
3690 #[arg(long = "dnssec-multi-signer")]
3691 pub dnssec_multi_signer: Option<bool>,
3692 #[arg(long = "dnssec-presigned")]
3693 pub dnssec_presigned: Option<bool>,
3694 #[arg(long = "dnssec-use-nsec3")]
3695 pub dnssec_use_nsec3: Option<bool>,
3696 #[arg(long)]
3697 pub token: Option<String>,
3698}
3699
3700#[derive(Args, Debug)]
3701#[command(
3702 about = "Inspect or edit provider DNS settings for a zone",
3703 arg_required_else_help = true,
3704 help_template = DNS_HELP_TEMPLATE,
3705 after_help = DNS_SETTINGS_AFTER_HELP
3706)]
3707pub struct DnsSettingsCmd {
3708 #[command(subcommand)]
3709 pub command: DnsSettingsSubCommand,
3710}
3711
3712#[derive(Subcommand, Debug)]
3713pub enum DnsSettingsSubCommand {
3714 #[command(about = "Fetch provider DNS settings for a zone")]
3715 Get(DnsSettingsGetCmd),
3716 #[command(about = "Edit provider DNS settings for a zone")]
3717 Edit(DnsSettingsEditCmd),
3718}
3719
3720#[derive(Args, Debug)]
3721pub struct DnsSettingsGetCmd {
3722 #[arg(long, value_enum, default_value = "cloudflare")]
3723 pub provider: DnsProviderKind,
3724 #[arg(long)]
3725 pub zone_id: String,
3726 #[arg(long)]
3727 pub token: Option<String>,
3728}
3729
3730#[derive(Args, Debug)]
3731pub struct DnsSettingsEditCmd {
3732 #[arg(long, value_enum, default_value = "cloudflare")]
3733 pub provider: DnsProviderKind,
3734 #[arg(long)]
3735 pub zone_id: String,
3736 #[arg(long)]
3737 pub flatten_all_cnames: Option<bool>,
3738 #[arg(long)]
3739 pub foundation_dns: Option<bool>,
3740 #[arg(long)]
3741 pub multi_provider: Option<bool>,
3742 #[arg(long)]
3743 pub ns_ttl: Option<u32>,
3744 #[arg(long)]
3745 pub secondary_overrides: Option<bool>,
3746 #[arg(long)]
3747 pub zone_mode: Option<String>,
3748 #[arg(long = "reference-zone-id")]
3749 pub reference_zone_id: Option<String>,
3750 #[arg(long = "nameservers-type")]
3751 pub nameservers_type: Option<String>,
3752 #[arg(long = "nameservers-ns-set")]
3753 pub nameservers_ns_set: Option<u32>,
3754 #[arg(long = "soa-json")]
3755 pub soa_json: Option<String>,
3756 #[arg(long)]
3757 pub token: Option<String>,
3758}
3759
3760#[derive(Args, Debug)]
3761#[command(
3762 arg_required_else_help = true,
3763 disable_help_subcommand = true,
3764 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3765 after_help = crate::cli::help_render::DOMAINS_AFTER_HELP
3766)]
3767pub struct DomainsCmd {
3768 #[arg(long, value_enum, default_value = "cloudflare")]
3769 pub provider: DomainsProviderKind,
3770 #[arg(long)]
3771 pub account_id: Option<String>,
3772 #[arg(long)]
3773 pub token: Option<String>,
3774 #[command(subcommand)]
3775 pub command: DomainsSubCommand,
3776}
3777
3778#[derive(Copy, Clone, Debug, Eq, PartialEq, ValueEnum)]
3779pub enum DomainsProviderKind {
3780 Cloudflare,
3781}
3782
3783#[derive(Subcommand, Debug)]
3784pub enum DomainsSubCommand {
3785 Search(DomainsSearchCmd),
3786 Check(DomainsCheckCmd),
3787 List(DomainsListCmd),
3788}
3789
3790#[derive(Args, Debug)]
3791pub struct DomainsSearchCmd {
3792 #[arg(long)]
3793 pub query: String,
3794 #[arg(long = "extension")]
3795 pub extensions: Vec<String>,
3796 #[arg(long)]
3797 pub limit: Option<usize>,
3798}
3799
3800#[derive(Args, Debug)]
3801pub struct DomainsCheckCmd {
3802 #[arg(long = "domain", required = true)]
3803 pub domains: Vec<String>,
3804}
3805
3806#[derive(Args, Debug)]
3807pub struct DomainsListCmd {}
3808
3809#[derive(Args, Debug)]
3810pub struct GenerateSystemdCmd {
3811 #[arg(
3812 long,
3813 default_value = "/etc/systemd/system",
3814 help = "Directory where the systemd units are written"
3815 )]
3816 pub output_dir: PathBuf,
3817 #[arg(long, help = "Only generate the unit for this service name")]
3818 pub service: Option<String>,
3819 #[arg(
3820 long,
3821 default_value_t = true,
3822 help = "Also generate the xbp-api systemd unit alongside project/services"
3823 )]
3824 pub api: bool,
3825}
3826
3827#[derive(Args, Debug)]
3828#[command(
3829 help_template = crate::cli::help_render::XBP_HELP_TEMPLATE,
3830 after_help = crate::cli::help_render::DONE_AFTER_HELP
3831)]
3832pub struct DoneCmd {
3833 #[arg(long, help = "Root directory under which to discover git repos")]
3834 pub root: Option<std::path::PathBuf>,
3835 #[arg(
3836 long,
3837 default_value = "24 hours ago",
3838 help = "Git --since value (e.g. '7 days ago')"
3839 )]
3840 pub since: String,
3841 #[arg(short, long, help = "Output Markdown file path")]
3842 pub output: Option<std::path::PathBuf>,
3843 #[arg(long, help = "Skip AI summarization (OpenRouter)")]
3844 pub no_ai: bool,
3845 #[arg(short, long, help = "Discover repos recursively")]
3846 pub recursive: bool,
3847 #[arg(long, help = "Exclude repo by name (repeatable)")]
3848 pub exclude: Vec<String>,
3849}
3850
3851#[derive(Args, Debug)]
3852pub struct FixProcessMonitorJsonCmd {
3853 #[arg(help = "Path to a Cursor process-monitor JSON export")]
3854 pub path: std::path::PathBuf,
3855 #[arg(
3856 long,
3857 help = "Check whether the file needs repair without writing changes"
3858 )]
3859 pub check: bool,
3860 #[arg(
3861 long,
3862 help = "Print repaired JSON to stdout instead of overwriting the file"
3863 )]
3864 pub stdout: bool,
3865}
3866
3867#[derive(Args, Debug)]
3868pub struct CursorCmd {
3869 #[command(subcommand)]
3870 pub command: CursorSubCommand,
3871}
3872
3873#[derive(Subcommand, Debug)]
3874pub enum CursorSubCommand {
3875 #[command(about = "Upload local Cursor file history to the XBP dashboard")]
3876 Ingest {
3877 #[arg(
3878 long,
3879 help = "Scan local Cursor history without uploading to the dashboard"
3880 )]
3881 dry_run: bool,
3882 },
3883}
3884
3885#[cfg(feature = "nordvpn")]
3886#[derive(Args, Debug)]
3887pub struct NordvpnCmd {
3888 #[arg(
3889 trailing_var_arg = true,
3890 allow_hyphen_values = true,
3891 help = "Subcommand or args to pass to nordvpn (e.g. setup, meshnet peer list)"
3892 )]
3893 pub args: Vec<String>,
3894}
3895
3896#[cfg(feature = "kubernetes")]
3897#[derive(Args, Debug)]
3898pub struct KubernetesCmd {
3899 #[command(subcommand)]
3900 pub command: KubernetesSubCommand,
3901}
3902
3903#[cfg(feature = "kubernetes")]
3904#[derive(Args, Debug)]
3905pub struct KubernetesAddonCmd {
3906 #[command(subcommand)]
3907 pub command: KubernetesAddonSubCommand,
3908}
3909
3910#[cfg(feature = "kubernetes")]
3911#[derive(Subcommand, Debug)]
3912pub enum KubernetesAddonSubCommand {
3913 List,
3915 Enable {
3917 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
3918 name: String,
3919 },
3920 Disable {
3922 #[arg(help = "Addon name (e.g. cert-manager, ingress, dashboard)")]
3923 name: String,
3924 },
3925}
3926
3927#[cfg(feature = "kubernetes")]
3928#[derive(Subcommand, Debug)]
3929pub enum KubernetesSubCommand {
3930 Check {
3932 #[arg(long, help = "Kubeconfig context to target")]
3933 context: Option<String>,
3934 #[arg(
3935 long,
3936 default_value = "default",
3937 help = "Namespace to probe for workload readiness"
3938 )]
3939 namespace: String,
3940 #[arg(long, help = "Skip live cluster calls (tooling check only)")]
3941 offline: bool,
3942 },
3943 Generate {
3945 #[arg(long, help = "Logical app name (used for resource names)")]
3946 name: String,
3947 #[arg(long, help = "Container image reference")]
3948 image: String,
3949 #[arg(long, default_value_t = 80, help = "Container port for the service")]
3950 port: u16,
3951 #[arg(long, default_value_t = 1, help = "Replica count")]
3952 replicas: u16,
3953 #[arg(
3954 long,
3955 default_value = "default",
3956 help = "Namespace for generated resources"
3957 )]
3958 namespace: String,
3959 #[arg(
3960 long,
3961 default_value = "k8s/xbp-manifest.yaml",
3962 help = "Path to write the manifest bundle"
3963 )]
3964 output: String,
3965 #[arg(long, help = "Optional ingress host (creates Ingress when set)")]
3966 host: Option<String>,
3967 },
3968 Apply {
3970 #[arg(long, help = "Path to manifest file")]
3971 file: String,
3972 #[arg(long, help = "Override kube context")]
3973 context: Option<String>,
3974 #[arg(long, help = "Override namespace")]
3975 namespace: Option<String>,
3976 #[arg(long, help = "Use --dry-run=server")]
3977 dry_run: bool,
3978 },
3979 Status {
3981 #[arg(long, default_value = "default", help = "Namespace to summarize")]
3982 namespace: String,
3983 #[arg(long, help = "Override kube context")]
3984 context: Option<String>,
3985 },
3986 Addons(KubernetesAddonCmd),
3988 DashboardToken {
3990 #[arg(
3991 long,
3992 default_value = "kube-system",
3993 help = "Namespace containing the dashboard token secret"
3994 )]
3995 namespace: String,
3996 #[arg(
3997 long,
3998 default_value = "microk8s-dashboard-token",
3999 help = "Secret name containing the dashboard login token"
4000 )]
4001 secret: String,
4002 #[arg(long, help = "Override kube context")]
4003 context: Option<String>,
4004 },
4005 ObservabilityCreds {
4007 #[arg(
4008 long,
4009 default_value = "observability",
4010 help = "Namespace containing Grafana secret"
4011 )]
4012 namespace: String,
4013 #[arg(
4014 long,
4015 default_value = "kube-prom-stack-grafana",
4016 help = "Grafana secret name"
4017 )]
4018 secret: String,
4019 #[arg(long, help = "Override kube context")]
4020 context: Option<String>,
4021 },
4022 Issuer {
4024 #[arg(
4025 long,
4026 help = "Email used for Let's Encrypt account registration (required)"
4027 )]
4028 email: String,
4029 #[arg(long, default_value = "letsencrypt", help = "Issuer resource name")]
4030 name: String,
4031 #[arg(
4032 long,
4033 default_value = "default",
4034 help = "Namespace for the Issuer resource"
4035 )]
4036 namespace: String,
4037 #[arg(
4038 long,
4039 default_value = "https://acme-v02.api.letsencrypt.org/directory",
4040 help = "ACME server URL (production by default)"
4041 )]
4042 server: String,
4043 #[arg(
4044 long,
4045 default_value = "letsencrypt-account-key",
4046 help = "Secret used to store the ACME account private key"
4047 )]
4048 private_key_secret: String,
4049 #[arg(
4050 long,
4051 default_value = "nginx",
4052 help = "Ingress class name used for HTTP01 solving"
4053 )]
4054 ingress_class_name: String,
4055 #[arg(long, help = "Override kube context")]
4056 context: Option<String>,
4057 #[arg(long, help = "Use --dry-run=server")]
4058 dry_run: bool,
4059 },
4060}
4061
4062#[cfg(test)]
4063mod tests {
4064 use super::{
4065 Cli, CloudflareConfigAction, CloudflaredSubCommand, Commands, DnsProviderKind,
4066 DnsSubCommand, DnsZonesSubCommand, DomainsProviderKind, DomainsSubCommand,
4067 GenerateSubCommand, LinearConfigAction, NetworkFloatingIpSubCommand,
4068 NetworkHetznerSubCommand, NetworkHetznerVswitchSubCommand, NetworkSubCommand, SshCmd,
4069 };
4070 #[cfg(feature = "secrets")]
4071 use super::{
4072 CloudflareSecretsSubCommand, SecretsProviderKind, SecretsStoresSubCommand,
4073 SecretsSubCommand,
4074 };
4075 use clap::Parser;
4076 use std::path::PathBuf;
4077
4078 #[test]
4079 fn parses_network_floating_ip_add() {
4080 let cli = Cli::parse_from([
4081 "xbp",
4082 "network",
4083 "floating-ip",
4084 "add",
4085 "--ip",
4086 "1.2.3.4",
4087 "--apply",
4088 ]);
4089
4090 match cli.command {
4091 Some(Commands::Network(network)) => match network.command {
4092 NetworkSubCommand::FloatingIp(fip) => match fip.command {
4093 NetworkFloatingIpSubCommand::Add { ip, apply, .. } => {
4094 assert_eq!(ip, "1.2.3.4");
4095 assert!(apply);
4096 }
4097 _ => panic!("expected add subcommand"),
4098 },
4099 _ => panic!("expected floating-ip subcommand"),
4100 },
4101 _ => panic!("expected network command"),
4102 }
4103 }
4104
4105 #[test]
4106 fn parses_generate_config_update() {
4107 let cli = Cli::parse_from(["xbp", "generate", "config", "--update"]);
4108
4109 match cli.command {
4110 Some(Commands::Generate(generate_cmd)) => match generate_cmd.command {
4111 GenerateSubCommand::Config(config_cmd) => assert!(config_cmd.update),
4112 _ => panic!("expected generate config command"),
4113 },
4114 _ => panic!("expected generate command"),
4115 }
4116 }
4117
4118 #[test]
4119 fn parses_commit_command_with_dry_run() {
4120 let cli = Cli::parse_from(["xbp", "commit", "--dry-run", "--scope", "cli"]);
4121
4122 match cli.command {
4123 Some(Commands::Commit(commit_cmd)) => {
4124 assert!(commit_cmd.dry_run);
4125 assert_eq!(commit_cmd.scope.as_deref(), Some("cli"));
4126 assert_eq!(commit_cmd.model, None);
4127 }
4128 _ => panic!("expected commit command"),
4129 }
4130 }
4131
4132 #[test]
4133 fn parses_linear_select_initiative_config_command() {
4134 let cli = Cli::parse_from(["xbp", "config", "linear", "select-initiative"]);
4135
4136 match cli.command {
4137 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
4138 Some(super::ConfigProviderCmd::Linear(linear_cmd)) => {
4139 assert!(matches!(
4140 linear_cmd.action,
4141 LinearConfigAction::SelectInitiative
4142 ));
4143 }
4144 _ => panic!("expected linear config provider"),
4145 },
4146 _ => panic!("expected config command"),
4147 }
4148 }
4149
4150 #[test]
4151 fn parses_ssh_command_with_cloudflared_and_key_auth() {
4152 let cli = Cli::parse_from([
4153 "xbp",
4154 "ssh",
4155 "--host",
4156 "ssh.internal",
4157 "--username",
4158 "deploy",
4159 "--private-key",
4160 "C:/Users/floris/.ssh/id_ed25519",
4161 "--cloudflared-hostname",
4162 "bastion.example.com",
4163 "--command",
4164 "htop",
4165 ]);
4166
4167 let Some(Commands::Ssh(SshCmd {
4168 ssh_host,
4169 ssh_username,
4170 private_key,
4171 cloudflared_hostname,
4172 command,
4173 ..
4174 })) = cli.command
4175 else {
4176 panic!("expected shell command");
4177 };
4178
4179 assert_eq!(ssh_host.as_deref(), Some("ssh.internal"));
4180 assert_eq!(ssh_username.as_deref(), Some("deploy"));
4181 assert_eq!(
4182 private_key,
4183 Some(PathBuf::from("C:/Users/floris/.ssh/id_ed25519"))
4184 );
4185 assert_eq!(cloudflared_hostname.as_deref(), Some("bastion.example.com"));
4186 assert_eq!(command.as_deref(), Some("htop"));
4187 }
4188
4189 #[test]
4190 fn parses_cloudflared_tcp_command() {
4191 let cli = Cli::parse_from([
4192 "xbp",
4193 "cloudflared",
4194 "tcp",
4195 "--hostname",
4196 "bastion.example.com",
4197 "--listener",
4198 "127.0.0.1:2222",
4199 ]);
4200
4201 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
4202 panic!("expected cloudflared command");
4203 };
4204
4205 match cloudflared_cmd.command {
4206 CloudflaredSubCommand::Tcp(tcp_cmd) => {
4207 assert_eq!(tcp_cmd.hostname.as_deref(), Some("bastion.example.com"));
4208 assert_eq!(tcp_cmd.listener.as_deref(), Some("127.0.0.1:2222"));
4209 }
4210 }
4211 }
4212
4213 #[test]
4214 fn parses_cloudflared_tcp_without_hostname_for_handler_validation() {
4215 let cli = Cli::try_parse_from(["xbp", "cloudflared", "tcp"]).expect("parse");
4216
4217 let Some(Commands::Cloudflared(cloudflared_cmd)) = cli.command else {
4218 panic!("expected cloudflared command");
4219 };
4220
4221 match cloudflared_cmd.command {
4222 CloudflaredSubCommand::Tcp(tcp_cmd) => {
4223 assert_eq!(tcp_cmd.hostname, None);
4224 assert_eq!(tcp_cmd.listener, None);
4225 }
4226 }
4227 }
4228
4229 #[test]
4230 fn parses_version_workspace_publish_run_command() {
4231 let cli = Cli::parse_from([
4232 "xbp",
4233 "version",
4234 "workspace",
4235 "publish",
4236 "run",
4237 "--repo",
4238 "C:/Users/floris/Documents/GitHub/athena",
4239 "--dry-run",
4240 "--from",
4241 "athena-s3",
4242 ]);
4243
4244 let Some(Commands::Version(version_cmd)) = cli.command else {
4245 panic!("expected version command");
4246 };
4247
4248 match version_cmd.command {
4249 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
4250 match workspace_cmd.command {
4251 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
4252 match publish_cmd.command {
4253 super::VersionWorkspacePublishSubCommand::Run(run_cmd) => {
4254 assert_eq!(
4255 run_cmd.target.repo,
4256 Some(PathBuf::from("C:/Users/floris/Documents/GitHub/athena"))
4257 );
4258 assert!(!run_cmd.target.json);
4259 assert!(run_cmd.dry_run);
4260 assert_eq!(run_cmd.from.as_deref(), Some("athena-s3"));
4261 }
4262 _ => panic!("expected publish run"),
4263 }
4264 }
4265 _ => panic!("expected workspace publish"),
4266 }
4267 }
4268 _ => panic!("expected version workspace command"),
4269 }
4270 }
4271
4272 #[test]
4273 fn parses_version_workspace_publish_plan_with_only_and_include_prereqs() {
4274 let cli = Cli::parse_from([
4275 "xbp",
4276 "version",
4277 "workspace",
4278 "publish",
4279 "plan",
4280 "--repo",
4281 "C:/Users/floris/Documents/GitHub/athena-auth",
4282 "--only",
4283 "athena-auth",
4284 "--include-prereqs",
4285 ]);
4286
4287 let Some(Commands::Version(version_cmd)) = cli.command else {
4288 panic!("expected version command");
4289 };
4290
4291 match version_cmd.command {
4292 Some(super::VersionSubCommand::Workspace(workspace_cmd)) => {
4293 match workspace_cmd.command {
4294 super::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
4295 match publish_cmd.command {
4296 super::VersionWorkspacePublishSubCommand::Plan(plan_cmd) => {
4297 assert_eq!(
4298 plan_cmd.target.repo,
4299 Some(PathBuf::from(
4300 "C:/Users/floris/Documents/GitHub/athena-auth"
4301 ))
4302 );
4303 assert_eq!(plan_cmd.only.as_deref(), Some("athena-auth"));
4304 assert!(plan_cmd.include_prereqs);
4305 }
4306 _ => panic!("expected publish plan"),
4307 }
4308 }
4309 _ => panic!("expected workspace publish"),
4310 }
4311 }
4312 _ => panic!("expected version workspace command"),
4313 }
4314 }
4315
4316 #[test]
4317 fn parses_commit_alias_with_push_flag() {
4318 let cli = Cli::parse_from(["xbp", "c", "-p"]);
4319
4320 let Some(Commands::Commit(commit_cmd)) = cli.command else {
4321 panic!("expected commit command");
4322 };
4323
4324 assert!(commit_cmd.push);
4325 assert!(!commit_cmd.dry_run);
4326 }
4327
4328 #[test]
4329 fn parses_version_alias_release_alias() {
4330 let cli = Cli::parse_from(["xbp", "v", "r", "--draft", "--publish", "--force"]);
4331
4332 let Some(Commands::Version(version_cmd)) = cli.command else {
4333 panic!("expected version command");
4334 };
4335
4336 let Some(super::VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
4337 panic!("expected release subcommand");
4338 };
4339
4340 assert!(release_cmd.draft);
4341 assert!(release_cmd.publish);
4342 assert!(release_cmd.force);
4343 }
4344
4345 #[test]
4346 fn parses_publish_command_target_filter() {
4347 let cli = Cli::parse_from([
4348 "xbp",
4349 "publish",
4350 "--allow-dirty",
4351 "--force",
4352 "--include-prereqs",
4353 "--target",
4354 "npm",
4355 "--manifest-path",
4356 "apps/web/package.json",
4357 ]);
4358
4359 let Some(Commands::Publish(publish_cmd)) = cli.command else {
4360 panic!("expected publish command");
4361 };
4362
4363 assert!(publish_cmd.allow_dirty);
4364 assert!(publish_cmd.force);
4365 assert!(publish_cmd.include_prereqs);
4366 assert_eq!(publish_cmd.target.as_deref(), Some("npm"));
4367 assert_eq!(
4368 publish_cmd.manifest_path,
4369 Some(PathBuf::from("apps/web/package.json"))
4370 );
4371 }
4372
4373 #[test]
4374 fn parses_npm_setup_release_config_command() {
4375 let cli = Cli::parse_from(["xbp", "config", "npm", "setup-release"]);
4376
4377 let Some(Commands::Config(config_cmd)) = cli.command else {
4378 panic!("expected config command");
4379 };
4380 let Some(super::ConfigProviderCmd::Npm(registry_cmd)) = config_cmd.provider else {
4381 panic!("expected npm config command");
4382 };
4383
4384 assert!(matches!(
4385 registry_cmd.action,
4386 super::RegistryConfigAction::SetupRelease
4387 ));
4388 }
4389
4390 #[test]
4391 fn parses_crates_login_config_command() {
4392 let cli = Cli::parse_from(["xbp", "config", "crates", "login"]);
4393
4394 let Some(Commands::Config(config_cmd)) = cli.command else {
4395 panic!("expected config command");
4396 };
4397 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
4398 panic!("expected crates config command");
4399 };
4400
4401 assert!(matches!(
4402 crates_cmd.action,
4403 super::CratesConfigAction::Login { .. }
4404 ));
4405 }
4406
4407 #[test]
4408 fn parses_crates_logout_config_command() {
4409 let cli = Cli::parse_from(["xbp", "config", "crates", "logout"]);
4410
4411 let Some(Commands::Config(config_cmd)) = cli.command else {
4412 panic!("expected config command");
4413 };
4414 let Some(super::ConfigProviderCmd::Crates(crates_cmd)) = config_cmd.provider else {
4415 panic!("expected crates config command");
4416 };
4417
4418 assert!(matches!(
4419 crates_cmd.action,
4420 super::CratesConfigAction::Logout
4421 ));
4422 }
4423
4424 #[test]
4425 fn parses_whoami_command() {
4426 let cli = Cli::parse_from(["xbp", "whoami"]);
4427
4428 assert!(matches!(cli.command, Some(Commands::Whoami)));
4429 }
4430
4431 #[test]
4432 fn parses_shell_alias_as_ssh_command() {
4433 let cli = Cli::parse_from(["xbp", "shell", "--host", "ssh.internal"]);
4434
4435 let Some(Commands::Ssh(ssh_cmd)) = cli.command else {
4436 panic!("expected ssh command through shell alias");
4437 };
4438
4439 assert_eq!(ssh_cmd.ssh_host.as_deref(), Some("ssh.internal"));
4440 }
4441
4442 #[test]
4443 fn parses_api_request_command() {
4444 let cli = Cli::parse_from([
4445 "xbp",
4446 "api",
4447 "request",
4448 "/api/registry/installers/python-pip",
4449 "--web",
4450 "--method",
4451 "GET",
4452 "--header",
4453 "accept: application/json",
4454 ]);
4455
4456 let Some(Commands::Api(api_cmd)) = cli.command else {
4457 panic!("expected api command");
4458 };
4459
4460 match api_cmd.command {
4461 super::ApiSubCommand::Request(request_cmd) => {
4462 assert_eq!(request_cmd.path, "/api/registry/installers/python-pip");
4463 assert!(request_cmd.target.web);
4464 assert_eq!(request_cmd.method.as_deref(), Some("GET"));
4465 assert_eq!(
4466 request_cmd.target.header,
4467 vec!["accept: application/json".to_string()]
4468 );
4469 }
4470 _ => panic!("expected api request subcommand"),
4471 }
4472 }
4473
4474 #[test]
4475 fn parses_api_projects_list_command() {
4476 let cli = Cli::parse_from([
4477 "xbp",
4478 "api",
4479 "projects",
4480 "list",
4481 "--organization-id",
4482 "org_123",
4483 ]);
4484
4485 let Some(Commands::Api(api_cmd)) = cli.command else {
4486 panic!("expected api command");
4487 };
4488
4489 match api_cmd.command {
4490 super::ApiSubCommand::Projects(projects_cmd) => match projects_cmd.command {
4491 super::ApiProjectsSubCommand::List(list_cmd) => {
4492 assert_eq!(list_cmd.organization_id.as_deref(), Some("org_123"));
4493 }
4494 _ => panic!("expected projects list subcommand"),
4495 },
4496 _ => panic!("expected projects subcommand"),
4497 }
4498 }
4499
4500 #[test]
4501 fn parses_api_routes_create_command() {
4502 let cli = Cli::parse_from([
4503 "xbp",
4504 "api",
4505 "routes",
4506 "create",
4507 "--domain",
4508 "demo.local",
4509 "--target",
4510 "http://127.0.0.1:3000",
4511 "--weighted-target",
4512 "http://127.0.0.1:3001=3",
4513 "--base-url",
4514 "http://127.0.0.1:8080",
4515 ]);
4516
4517 let Some(Commands::Api(api_cmd)) = cli.command else {
4518 panic!("expected api command");
4519 };
4520
4521 match api_cmd.command {
4522 super::ApiSubCommand::Routes(routes_cmd) => match routes_cmd.command {
4523 super::ApiRoutesSubCommand::Create(create_cmd) => {
4524 assert_eq!(create_cmd.domain, "demo.local");
4525 assert_eq!(create_cmd.target, vec!["http://127.0.0.1:3000".to_string()]);
4526 assert_eq!(
4527 create_cmd.weighted_target,
4528 vec!["http://127.0.0.1:3001=3".to_string()]
4529 );
4530 assert_eq!(
4531 create_cmd.target_options.base_url.as_deref(),
4532 Some("http://127.0.0.1:8080")
4533 );
4534 }
4535 _ => panic!("expected routes create subcommand"),
4536 },
4537 _ => panic!("expected routes subcommand"),
4538 }
4539 }
4540
4541 #[test]
4542 fn parses_hetzner_vswitch_setup_command() {
4543 let cli = Cli::parse_from([
4544 "xbp",
4545 "network",
4546 "hetzner",
4547 "vswitch",
4548 "setup",
4549 "--ip",
4550 "10.0.3.2",
4551 "--vlan-id",
4552 "4000",
4553 "--interface",
4554 "enp0s31f6",
4555 "--apply",
4556 ]);
4557
4558 let Some(Commands::Network(network_cmd)) = cli.command else {
4559 panic!("expected network command");
4560 };
4561
4562 match network_cmd.command {
4563 NetworkSubCommand::Hetzner(hetzner_cmd) => match hetzner_cmd.command {
4564 NetworkHetznerSubCommand::Vswitch(vswitch_cmd) => match vswitch_cmd.command {
4565 NetworkHetznerVswitchSubCommand::Setup {
4566 ip,
4567 cidr,
4568 interface,
4569 vlan_id,
4570 apply,
4571 ..
4572 } => {
4573 assert_eq!(ip, "10.0.3.2");
4574 assert_eq!(cidr, 24);
4575 assert_eq!(interface.as_deref(), Some("enp0s31f6"));
4576 assert_eq!(vlan_id, 4000);
4577 assert!(apply);
4578 }
4579 },
4580 },
4581 _ => panic!("expected hetzner subcommand"),
4582 }
4583 }
4584
4585 #[cfg(feature = "secrets")]
4586 #[test]
4587 fn parses_secrets_diag_command() {
4588 let cli = Cli::parse_from(["xbp", "secrets", "diag"]);
4589
4590 match cli.command {
4591 Some(Commands::Secrets(secrets_cmd)) => {
4592 assert!(matches!(secrets_cmd.command, Some(SecretsSubCommand::Diag)));
4593 assert_eq!(secrets_cmd.environment, "xbp-dev");
4594 }
4595 _ => panic!("expected secrets command"),
4596 }
4597 }
4598
4599 #[cfg(feature = "secrets")]
4600 #[test]
4601 fn parses_secrets_environment_override() {
4602 let cli = Cli::parse_from(["xbp", "secrets", "--environment", "xbp-prod", "push"]);
4603
4604 match cli.command {
4605 Some(Commands::Secrets(secrets_cmd)) => {
4606 assert_eq!(secrets_cmd.environment, "xbp-prod");
4607 assert!(matches!(
4608 secrets_cmd.command,
4609 Some(SecretsSubCommand::Push(_))
4610 ));
4611 }
4612 _ => panic!("expected secrets command"),
4613 }
4614 }
4615
4616 #[test]
4617 fn parses_version_discover_command() {
4618 let cli = Cli::parse_from(["xbp", "version", "discover", "--dry-run"]);
4619
4620 match cli.command {
4621 Some(Commands::Version(version_cmd)) => match version_cmd.command {
4622 Some(super::VersionSubCommand::Discover(discover_cmd)) => {
4623 assert!(discover_cmd.dry_run);
4624 assert!(!discover_cmd.no_register);
4625 }
4626 _ => panic!("expected version discover subcommand"),
4627 },
4628 _ => panic!("expected version command"),
4629 }
4630 }
4631
4632 #[cfg(feature = "secrets")]
4633 #[test]
4634 fn parses_secrets_providers_command() {
4635 let cli = Cli::parse_from(["xbp", "secrets", "providers"]);
4636
4637 match cli.command {
4638 Some(Commands::Secrets(secrets_cmd)) => {
4639 assert!(matches!(
4640 secrets_cmd.command,
4641 Some(SecretsSubCommand::Providers)
4642 ));
4643 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Github);
4644 }
4645 _ => panic!("expected secrets command"),
4646 }
4647 }
4648
4649 #[cfg(feature = "secrets")]
4650 #[test]
4651 fn parses_cloudflare_secret_store_create() {
4652 let cli = Cli::parse_from([
4653 "xbp",
4654 "secrets",
4655 "--provider",
4656 "cloudflare",
4657 "stores",
4658 "create",
4659 "--name",
4660 "prod",
4661 ]);
4662
4663 match cli.command {
4664 Some(Commands::Secrets(secrets_cmd)) => {
4665 assert_eq!(secrets_cmd.provider, SecretsProviderKind::Cloudflare);
4666 match secrets_cmd.command {
4667 Some(SecretsSubCommand::Stores(stores_cmd)) => {
4668 assert!(matches!(
4669 stores_cmd.command,
4670 SecretsStoresSubCommand::Create(_)
4671 ));
4672 }
4673 _ => panic!("expected stores subcommand"),
4674 }
4675 }
4676 _ => panic!("expected secrets command"),
4677 }
4678 }
4679
4680 #[cfg(feature = "secrets")]
4681 #[test]
4682 fn parses_cloudflare_secret_duplicate() {
4683 let cli = Cli::parse_from([
4684 "xbp",
4685 "secrets",
4686 "--provider",
4687 "cloudflare",
4688 "secrets",
4689 "duplicate",
4690 "--store-id",
4691 "store_1",
4692 "--secret-id",
4693 "secret_1",
4694 "--name",
4695 "COPY",
4696 ]);
4697
4698 match cli.command {
4699 Some(Commands::Secrets(secrets_cmd)) => match secrets_cmd.command {
4700 Some(SecretsSubCommand::Secrets(secrets_cmd)) => {
4701 assert!(matches!(
4702 secrets_cmd.command,
4703 CloudflareSecretsSubCommand::Duplicate(_)
4704 ));
4705 }
4706 _ => panic!("expected cloudflare secrets subcommand"),
4707 },
4708 _ => panic!("expected secrets command"),
4709 }
4710 }
4711
4712 #[test]
4713 fn parses_workers_secret_put_from_stdin_command() {
4714 let cli = Cli::parse_from([
4715 "xbp",
4716 "workers",
4717 "secrets",
4718 "--environment",
4719 "production",
4720 "put",
4721 "--name",
4722 "API_KEY",
4723 "--from-stdin",
4724 ]);
4725
4726 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4727 panic!("expected workers command");
4728 };
4729
4730 match workers_cmd.command {
4731 super::WorkersSubCommand::Secrets(secrets_cmd) => {
4732 assert_eq!(
4733 secrets_cmd.target.environment.as_deref(),
4734 Some("production")
4735 );
4736 match secrets_cmd.command {
4737 super::WorkersSecretsSubCommand::Put(put_cmd) => {
4738 assert_eq!(put_cmd.name, "API_KEY");
4739 assert!(put_cmd.from_stdin);
4740 assert_eq!(put_cmd.value, None);
4741 }
4742 _ => panic!("expected workers secret put"),
4743 }
4744 }
4745 _ => panic!("expected workers secrets command"),
4746 }
4747 }
4748
4749 #[test]
4750 fn parses_workers_d1_migrations_local_command() {
4751 let cli = Cli::parse_from([
4752 "xbp",
4753 "workers",
4754 "d1",
4755 "migrations",
4756 "apply",
4757 "DB",
4758 "--local",
4759 "--environment",
4760 "preview",
4761 ]);
4762
4763 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4764 panic!("expected workers command");
4765 };
4766
4767 match workers_cmd.command {
4768 super::WorkersSubCommand::D1(d1_cmd) => match d1_cmd.command {
4769 super::WorkersD1SubCommand::Migrations(migrations_cmd) => {
4770 match migrations_cmd.command {
4771 super::WorkersD1MigrationsSubCommand::Apply(apply_cmd) => {
4772 assert_eq!(apply_cmd.database, "DB");
4773 assert!(apply_cmd.local);
4774 assert!(!apply_cmd.remote);
4775 assert_eq!(apply_cmd.target.environment.as_deref(), Some("preview"));
4776 }
4777 }
4778 }
4779 },
4780 _ => panic!("expected workers d1 command"),
4781 }
4782 }
4783
4784 #[test]
4785 fn parses_workers_deploy_ci_version_upload_command() {
4786 let cli = Cli::parse_from(["xbp", "workers", "deploy", "ci", "--version-upload"]);
4787
4788 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4789 panic!("expected workers command");
4790 };
4791
4792 match workers_cmd.command {
4793 super::WorkersSubCommand::Deploy(deploy_cmd) => match deploy_cmd.command {
4794 super::WorkersDeploySubCommand::Ci(ci_cmd) => {
4795 assert!(ci_cmd.version_upload);
4796 }
4797 _ => panic!("expected workers deploy ci command"),
4798 },
4799 _ => panic!("expected workers deploy command"),
4800 }
4801 }
4802
4803 #[test]
4804 fn parses_workers_list_alias_command() {
4805 let cli = Cli::parse_from(["xbp", "workers", "ls", "--all"]);
4806
4807 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4808 panic!("expected workers command");
4809 };
4810
4811 match workers_cmd.command {
4812 super::WorkersSubCommand::List(list_cmd) => {
4813 assert!(list_cmd.all);
4814 assert!(!list_cmd.json);
4815 }
4816 _ => panic!("expected workers list command"),
4817 }
4818 }
4819
4820 #[test]
4821 fn parses_workers_logs_follow_and_build_flags() {
4822 let cli = Cli::parse_from([
4823 "xbp",
4824 "workers",
4825 "logs",
4826 "-f",
4827 "--build",
4828 "--failed",
4829 "xbp-production",
4830 ]);
4831
4832 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4833 panic!("expected workers command");
4834 };
4835
4836 match workers_cmd.command {
4837 super::WorkersSubCommand::Logs(logs_cmd) => {
4838 assert!(logs_cmd.follow);
4839 assert!(logs_cmd.build);
4840 assert!(logs_cmd.failed);
4841 assert_eq!(logs_cmd.script_name.as_deref(), Some("xbp-production"));
4842 }
4843 _ => panic!("expected workers logs command"),
4844 }
4845 }
4846
4847 #[test]
4848 fn parses_workers_logs_worker_and_wait_flags() {
4849 let cli = Cli::parse_from([
4850 "xbp",
4851 "workers",
4852 "logs",
4853 "--worker",
4854 "suits-formations",
4855 "--json",
4856 "--wait",
4857 "--wait-seconds",
4858 "60",
4859 ]);
4860
4861 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4862 panic!("expected workers command");
4863 };
4864
4865 match workers_cmd.command {
4866 super::WorkersSubCommand::Logs(logs_cmd) => {
4867 assert_eq!(logs_cmd.target.worker.as_deref(), Some("suits-formations"));
4868 assert!(logs_cmd.json);
4869 assert!(logs_cmd.wait);
4870 assert_eq!(logs_cmd.wait_seconds, 60);
4871 }
4872 _ => panic!("expected workers logs command"),
4873 }
4874 }
4875
4876 #[test]
4877 fn parses_workers_logs_output_and_filter_flags() {
4878 let cli = Cli::parse_from([
4879 "xbp",
4880 "workers",
4881 "logs",
4882 "--build",
4883 "-n",
4884 "200",
4885 "-o",
4886 "build.log",
4887 "-g",
4888 "error",
4889 "--errors-only",
4890 "--list-builds",
4891 "--build-index",
4892 "1",
4893 ]);
4894
4895 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4896 panic!("expected workers command");
4897 };
4898
4899 match workers_cmd.command {
4900 super::WorkersSubCommand::Logs(logs_cmd) => {
4901 assert!(logs_cmd.build);
4902 assert_eq!(logs_cmd.lines, Some(200));
4903 assert_eq!(
4904 logs_cmd.output.as_deref().and_then(|path| path.to_str()),
4905 Some("build.log")
4906 );
4907 assert_eq!(logs_cmd.grep.as_deref(), Some("error"));
4908 assert!(logs_cmd.errors_only);
4909 assert!(logs_cmd.list_builds);
4910 assert_eq!(logs_cmd.build_index, Some(1));
4911 }
4912 _ => panic!("expected workers logs command"),
4913 }
4914 }
4915
4916 #[test]
4917 fn parses_workers_list_filter_and_limit_flags() {
4918 let cli = Cli::parse_from([
4919 "xbp", "workers", "list", "--failed", "-n", "5", "--sort", "modified",
4920 ]);
4921
4922 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4923 panic!("expected workers command");
4924 };
4925
4926 match workers_cmd.command {
4927 super::WorkersSubCommand::List(list_cmd) => {
4928 assert!(list_cmd.failed);
4929 assert_eq!(list_cmd.limit, Some(5));
4930 assert_eq!(list_cmd.sort, "modified");
4931 }
4932 _ => panic!("expected workers list command"),
4933 }
4934 }
4935
4936 #[test]
4937 fn parses_worker_alias_command() {
4938 let cli = Cli::parse_from(["xbp", "worker", "env", "--show-values"]);
4939
4940 let Some(Commands::Workers(workers_cmd)) = cli.command else {
4941 panic!("expected workers command through alias");
4942 };
4943
4944 match workers_cmd.command {
4945 super::WorkersSubCommand::Env(env_cmd) => {
4946 assert!(env_cmd.show_values);
4947 }
4948 _ => panic!("expected workers env command"),
4949 }
4950 }
4951
4952 #[test]
4953 fn parses_dns_providers_command() {
4954 let cli = Cli::parse_from(["xbp", "dns", "providers"]);
4955
4956 match cli.command {
4957 Some(Commands::Dns(dns_cmd)) => {
4958 assert!(matches!(dns_cmd.command, DnsSubCommand::Providers));
4959 }
4960 _ => panic!("expected dns command"),
4961 }
4962 }
4963
4964 #[test]
4965 fn dns_zone_list_defaults_provider_to_cloudflare() {
4966 let cli = Cli::parse_from(["xbp", "dns", "zones", "list"]);
4967
4968 let Some(Commands::Dns(dns_cmd)) = cli.command else {
4969 panic!("expected dns command");
4970 };
4971
4972 match dns_cmd.command {
4973 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
4974 DnsZonesSubCommand::List(list_cmd) => {
4975 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4976 }
4977 _ => panic!("expected zones list command"),
4978 },
4979 _ => panic!("expected zones command"),
4980 }
4981 }
4982
4983 #[test]
4984 fn dns_record_list_defaults_provider_to_cloudflare() {
4985 let cli = Cli::parse_from(["xbp", "dns", "records", "list", "--zone-id", "zone_123"]);
4986
4987 let Some(Commands::Dns(dns_cmd)) = cli.command else {
4988 panic!("expected dns command");
4989 };
4990
4991 match dns_cmd.command {
4992 DnsSubCommand::Records(records_cmd) => match records_cmd.command {
4993 super::DnsRecordsSubCommand::List(list_cmd) => {
4994 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
4995 assert_eq!(list_cmd.zone_id, "zone_123");
4996 }
4997 _ => panic!("expected records list command"),
4998 },
4999 _ => panic!("expected records command"),
5000 }
5001 }
5002
5003 #[test]
5004 fn dns_help_includes_descriptions_and_examples() {
5005 let err = Cli::try_parse_from(["xbp", "dns", "-h"]).expect_err("help");
5006 let rendered = err.to_string();
5007
5008 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
5009 assert!(rendered.contains("Manage DNS providers, zones, records, DNSSEC, and settings"));
5010 assert!(rendered.contains("List supported DNS providers and current implementation status"));
5011 assert!(rendered.contains("xbp dns records create"));
5012 }
5013
5014 #[test]
5015 fn dns_providers_help_includes_discovery_note() {
5016 let err = Cli::try_parse_from(["xbp", "dns", "providers", "-h"]).expect_err("help");
5017 let rendered = err.to_string();
5018
5019 assert!(matches!(err.kind(), clap::error::ErrorKind::DisplayHelp));
5020 assert!(rendered.contains("Implemented providers are wired into `xbp dns` today."));
5021 }
5022
5023 #[test]
5024 fn dns_records_without_subcommand_displays_help_screen() {
5025 let err = Cli::try_parse_from(["xbp", "dns", "records"]).expect_err("missing subcommand");
5026 let rendered = err.to_string();
5027
5028 assert!(matches!(
5029 err.kind(),
5030 clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
5031 | clap::error::ErrorKind::MissingSubcommand
5032 ));
5033 assert!(rendered.contains("List, create, edit, import, export, and batch DNS records"));
5034 assert!(rendered.contains("Create a new DNS record"));
5035 assert!(rendered.contains("xbp dns records import"));
5036 }
5037
5038 #[test]
5039 fn parses_dns_zone_list_command() {
5040 let cli = Cli::parse_from([
5041 "xbp",
5042 "dns",
5043 "zones",
5044 "list",
5045 "--provider",
5046 "cloudflare",
5047 "--account-name-op",
5048 "contains",
5049 "--type",
5050 "full,partial",
5051 ]);
5052
5053 match cli.command {
5054 Some(Commands::Dns(dns_cmd)) => match dns_cmd.command {
5055 DnsSubCommand::Zones(zones_cmd) => match zones_cmd.command {
5056 DnsZonesSubCommand::List(list_cmd) => {
5057 assert_eq!(list_cmd.provider, DnsProviderKind::Cloudflare);
5058 assert_eq!(list_cmd.account_name_op.as_deref(), Some("contains"));
5059 assert_eq!(list_cmd.zone_types, vec!["full", "partial"]);
5060 }
5061 _ => panic!("expected dns zones list"),
5062 },
5063 _ => panic!("expected dns zones"),
5064 },
5065 _ => panic!("expected dns command"),
5066 }
5067 }
5068
5069 #[test]
5070 fn parses_domains_search_command() {
5071 let cli = Cli::parse_from([
5072 "xbp",
5073 "domains",
5074 "search",
5075 "--query",
5076 "xbp",
5077 "--extension",
5078 "com",
5079 ]);
5080
5081 match cli.command {
5082 Some(Commands::Domains(domains_cmd)) => {
5083 assert_eq!(domains_cmd.provider, DomainsProviderKind::Cloudflare);
5084 assert!(matches!(domains_cmd.command, DomainsSubCommand::Search(_)));
5085 }
5086 _ => panic!("expected domains command"),
5087 }
5088 }
5089
5090 #[test]
5091 fn parses_cloudflare_config_account_id_command() {
5092 let cli = Cli::parse_from(["xbp", "config", "cloudflare", "set-account-id", "acc_123"]);
5093
5094 match cli.command {
5095 Some(Commands::Config(config_cmd)) => match config_cmd.provider {
5096 Some(super::ConfigProviderCmd::Cloudflare(cloudflare_cmd)) => {
5097 assert!(matches!(
5098 cloudflare_cmd.action,
5099 Some(CloudflareConfigAction::SetAccountId { .. })
5100 ));
5101 }
5102 _ => panic!("expected cloudflare config provider"),
5103 },
5104 _ => panic!("expected config command"),
5105 }
5106 }
5107
5108 #[test]
5109 fn parses_runners_groups_update_command() {
5110 let cli = Cli::parse_from([
5111 "xbp",
5112 "runners",
5113 "groups",
5114 "update",
5115 "--organization-id",
5116 "org_123",
5117 "--name",
5118 "linux-builders",
5119 "--visibility",
5120 "selected",
5121 "--restricted-to-workflows",
5122 ]);
5123
5124 let Some(Commands::Runners(runners_cmd)) = cli.command else {
5125 panic!("expected runners command");
5126 };
5127
5128 match runners_cmd.command {
5129 super::RunnersSubCommand::Groups(groups_cmd) => match groups_cmd.command {
5130 super::RunnersGroupsSubCommand::Update(update_cmd) => {
5131 assert_eq!(update_cmd.organization_id, "org_123");
5132 assert_eq!(update_cmd.name, "linux-builders");
5133 assert_eq!(update_cmd.visibility.as_deref(), Some("selected"));
5134 assert!(update_cmd.restricted_to_workflows);
5135 }
5136 _ => panic!("expected runners groups update command"),
5137 },
5138 _ => panic!("expected runners groups command"),
5139 }
5140 }
5141
5142 #[test]
5143 fn parses_api_runners_group_repositories_create_command() {
5144 let cli = Cli::parse_from([
5145 "xbp",
5146 "api",
5147 "runners",
5148 "group-repositories-create",
5149 "group_123",
5150 "--repository-owner",
5151 "xylex-group",
5152 "--repository-name",
5153 "xbp",
5154 "--repository-full-name",
5155 "xylex-group/xbp",
5156 "--is-private",
5157 ]);
5158
5159 let Some(Commands::Api(api_cmd)) = cli.command else {
5160 panic!("expected api command");
5161 };
5162
5163 match api_cmd.command {
5164 super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
5165 super::ApiRunnersSubCommand::GroupRepositoriesCreate(create_cmd) => {
5166 assert_eq!(create_cmd.runner_group_id, "group_123");
5167 assert_eq!(create_cmd.repository_owner, "xylex-group");
5168 assert_eq!(create_cmd.repository_name, "xbp");
5169 assert_eq!(create_cmd.repository_full_name, "xylex-group/xbp");
5170 assert!(create_cmd.is_private);
5171 }
5172 _ => panic!("expected api runners group repositories create command"),
5173 },
5174 _ => panic!("expected api runners command"),
5175 }
5176 }
5177
5178 #[test]
5179 fn parses_runners_host_preflight_command() {
5180 let cli = Cli::parse_from([
5181 "xbp",
5182 "runners",
5183 "hosts",
5184 "preflight",
5185 "--runner-host-id",
5186 "host_123",
5187 "--write-api",
5188 ]);
5189
5190 let Some(Commands::Runners(runners_cmd)) = cli.command else {
5191 panic!("expected runners command");
5192 };
5193
5194 match runners_cmd.command {
5195 super::RunnersSubCommand::Hosts(hosts_cmd) => match hosts_cmd.command {
5196 super::RunnersHostsSubCommand::Preflight(preflight_cmd) => {
5197 assert_eq!(preflight_cmd.runner_host_id, "host_123");
5198 assert!(preflight_cmd.write_api);
5199 }
5200 _ => panic!("expected runners hosts preflight command"),
5201 },
5202 _ => panic!("expected runners hosts command"),
5203 }
5204 }
5205
5206 #[test]
5207 fn parses_runners_agent_serve_command() {
5208 let cli = Cli::parse_from([
5209 "xbp",
5210 "runners",
5211 "agent",
5212 "serve",
5213 "--runner-host-id",
5214 "host_123",
5215 "--interval-seconds",
5216 "5",
5217 "--once",
5218 ]);
5219
5220 let Some(Commands::Runners(runners_cmd)) = cli.command else {
5221 panic!("expected runners command");
5222 };
5223
5224 match runners_cmd.command {
5225 super::RunnersSubCommand::Agent(agent_cmd) => match agent_cmd.command {
5226 super::RunnersAgentSubCommand::Serve(serve_cmd) => {
5227 assert_eq!(serve_cmd.runner_host_id, "host_123");
5228 assert_eq!(serve_cmd.interval_seconds, 5);
5229 assert!(serve_cmd.once);
5230 }
5231 },
5232 _ => panic!("expected runners agent command"),
5233 }
5234 }
5235
5236 #[test]
5237 fn parses_api_runners_host_preflight_upsert_command() {
5238 let cli = Cli::parse_from([
5239 "xbp",
5240 "api",
5241 "runners",
5242 "hosts-preflights-upsert",
5243 "--runner-host-id",
5244 "host_123",
5245 "--platform",
5246 "linux",
5247 "--status",
5248 "synced",
5249 "--service-manager",
5250 "systemd",
5251 ]);
5252
5253 let Some(Commands::Api(api_cmd)) = cli.command else {
5254 panic!("expected api command");
5255 };
5256
5257 match api_cmd.command {
5258 super::ApiSubCommand::Runners(runners_cmd) => match runners_cmd.command {
5259 super::ApiRunnersSubCommand::HostsPreflightsUpsert(upsert_cmd) => {
5260 assert_eq!(upsert_cmd.runner_host_id, "host_123");
5261 assert_eq!(upsert_cmd.platform, "linux");
5262 assert_eq!(upsert_cmd.status, "synced");
5263 assert_eq!(upsert_cmd.service_manager.as_deref(), Some("systemd"));
5264 }
5265 _ => panic!("expected api runners host preflight upsert command"),
5266 },
5267 _ => panic!("expected api runners command"),
5268 }
5269 }
5270}