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