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