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