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