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