1pub mod app;
2pub mod auto_commit;
3pub mod commands;
4pub mod error;
5pub mod features;
6pub mod handlers;
7pub mod help_render;
8pub mod router;
9pub mod ui;
10
11pub use handlers::*;
12
13use crate::cli::app::AppContext;
14use crate::cli::error::{CliError, CliResult, ErrorFactory};
15use crate::commands::cli_session::{
16 require_authenticated_cli_session, run_login_status, run_logout, run_whoami,
17};
18use crate::commands::curl;
19#[cfg(feature = "openapi-gen")]
20use crate::commands::generate_openapi::{run_generate_openapi, GenerateOpenApiArgs};
21use crate::commands::generate_systemd::{run_generate_systemd, GenerateSystemdArgs};
22use crate::commands::redeploy_v2::run_redeploy_v2;
23use crate::commands::{
24 install_package, list_services, open_global_config, pick_service_for_command, run_commit,
25 run_config, run_config_cloudflare_account_delete, run_config_cloudflare_account_set,
26 run_config_cloudflare_account_show, run_config_cloudflare_login, run_config_cloudflare_setup,
27 run_config_cloudflare_status, run_config_crates_login, run_config_crates_logout,
28 run_config_linear_select_initiative, run_config_openapi_setup, run_config_openapi_show,
29 run_config_publish_setup, run_config_release_setup, run_config_secret_delete,
30 run_config_secret_set, run_config_secret_show, run_cursor_ingest,
31 run_generate_config, run_init, run_login, run_publish_command, run_redeploy,
32 run_redeploy_service, run_service_command, run_service_interactive, run_setup,
33 run_version_bump_command, run_version_command, run_version_discover_services,
34 run_version_domain_command, run_version_release_command, run_version_workspace_command,
35 show_service_help, CommitArgs, CommitError, CommitRunOutcome, GenerateConfigArgs,
36 PublishCommandOptions, ReleaseLatestPolicy, VersionDomainCommand, VersionDomainCommandOptions,
37 VersionDomainDiagnoseOptions, VersionDomainDoctorOptions, VersionDomainInitOptions,
38 VersionDomainReleaseOptions, VersionDomainSyncOptions, VersionReleaseOptions,
39 WorkspacePublishPlanOptions, WorkspacePublishRunOptions, WorkspaceVersionCheckOptions,
40 WorkspaceVersionCommand, WorkspaceVersionCommandOptions, WorkspaceVersionSyncOptions,
41 WorkspaceVersionValidateOptions,
42};
43use crate::commands::{run_diag, run_nginx};
44use crate::config::sync_versioning_files_registry;
45use crate::logging::{init_logger, log_error, log_info, log_success, log_warn};
46use clap::{error::ErrorKind as ClapErrorKind, Parser};
47use colored::Colorize;
48use commands::Cli;
49
50pub async fn run() -> CliResult<()> {
51 let cli: Cli = match Cli::try_parse() {
52 Ok(cli) => cli,
53 Err(err) => {
54 let kind = err.kind();
55 let rendered = err.to_string();
56 if kind == ClapErrorKind::DisplayVersion {
57 help_render::emit_version_line(env!("CARGO_PKG_VERSION"));
58 return Ok(());
59 }
60 if kind == ClapErrorKind::DisplayHelp
61 || kind == ClapErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
62 || rendered.contains("Manage the XBP API server")
63 {
64 ui::configure_color_output();
65 if help_render::is_root_help_text(&rendered) {
66 crate::commands::print_help().await;
67 } else {
68 help_render::emit_styled_help(&rendered, help_render::HelpScope::Auto);
69 }
70 return Ok(());
71 }
72 return Err(ErrorFactory::clap_parse(err));
73 }
74 };
75
76 if cli.commands {
77 help_render::print_command_catalog();
78 return Ok(());
79 }
80
81 if should_print_help(&cli) {
84 ui::configure_color_output();
85 crate::commands::print_help().await;
86 return Ok(());
87 }
88
89 let debug: bool = cli.debug;
90 ui::configure_color_output();
91 let command_name = cli
92 .command
93 .as_ref()
94 .map(commands::command_label)
95 .unwrap_or("interactive");
96 ui::print_cli_header(command_name, debug);
97
98 if let Err(e) = init_logger(debug).await {
99 let _ = log_error(
100 "system",
101 "Failed to initialize logger",
102 Some(&e.to_string()),
103 )
104 .await;
105 }
106 if let Err(e) = sync_versioning_files_registry() {
107 let _ = log_warn("config", "Failed to sync versioning registry", Some(&e)).await;
108 }
109
110 let background_cursor_trigger =
111 background_cursor_ingest_trigger(cli.command.as_ref()).map(str::to_string);
112 let background_system_inventory_trigger =
113 background_system_inventory_refresh_trigger(cli.command.as_ref()).map(str::to_string);
114 let mut ctx = AppContext::new(debug);
115 let result = router::dispatch(cli, &mut ctx).await;
116
117 if result.is_ok() {
118 if let Some(trigger) = background_cursor_trigger {
119 if let Err(error) =
120 crate::commands::cursor_ingest::maybe_start_background_cursor_ingest(&trigger)
121 {
122 let _ = log_warn(
123 "cursor",
124 "Background Cursor ingest could not be started",
125 Some(&error),
126 )
127 .await;
128 }
129 }
130 if let Some(trigger) = background_system_inventory_trigger {
131 if let Err(error) = crate::commands::system_inventory_refresh::
132 maybe_start_background_system_inventory_refresh(&trigger)
133 {
134 let _ = log_warn(
135 "codetime",
136 "Background system inventory refresh could not be started",
137 Some(&error),
138 )
139 .await;
140 }
141 }
142 }
143
144 result
145}
146
147pub(super) async fn handle_init(debug: bool) -> CliResult<()> {
148 if let Err(e) = run_init(debug).await {
149 let _ = log_error("init", "Init failed", Some(&e)).await;
150 return Err(e.into());
151 }
152 Ok(())
153}
154
155pub(super) async fn handle_commit(cmd: commands::CommitCmd) -> CliResult<()> {
156 let args = CommitArgs {
157 dry_run: cmd.dry_run,
158 push: cmd.push,
159 no_ai: cmd.no_ai,
160 model: cmd.model,
161 scope: cmd.scope,
162 };
163
164 match run_commit(args).await {
165 Ok(CommitRunOutcome::Completed) | Ok(CommitRunOutcome::NothingToCommit) => Ok(()),
166 Err(error) => Err(map_commit_error(error)),
167 }
168}
169
170fn map_commit_error(error: CommitError) -> CliError {
171 match error {
172 CommitError::PushFailed {
173 summary,
174 commit_sha,
175 } => {
176 let details = if let Some(commit_sha) = commit_sha {
177 format!("Created local commit {commit_sha} but push failed: {summary}")
178 } else {
179 format!("Push failed: {summary}")
180 };
181 ErrorFactory::operation(
182 "commit",
183 "push changes",
184 details,
185 commit_push_hint(&summary),
186 )
187 }
188 CommitError::TerminalSessionsBlocked(message) => ErrorFactory::validation(
189 "commit",
190 message,
191 Some("Add `/terminals` to `.gitignore` and run `git rm -r --cached terminals/`."),
192 ),
193 CommitError::Operation(message) => ErrorFactory::operation(
194 "commit",
195 "create conventional commit",
196 message,
197 Some("Run `xbp commit --dry-run` after saving changes."),
198 ),
199 }
200}
201
202fn commit_push_hint(summary: &str) -> Option<&'static str> {
203 let lowered = summary.to_ascii_lowercase();
204 if lowered.contains("auto-rebase failed") || lowered.contains("conflict") {
205 Some("Resolve rebase conflicts, run `git rebase --continue`, then `git push`.")
206 } else if lowered.contains("behind") || lowered.contains("non-fast-forward") {
207 Some("XBP should auto-rebase; if it did not, run `git pull --rebase` then `git push`.")
208 } else {
209 Some("Verify GitHub auth and branch permissions, then push again.")
210 }
211}
212
213pub(super) async fn handle_publish(cmd: commands::PublishCmd, _debug: bool) -> CliResult<()> {
214 let options = PublishCommandOptions {
215 dry_run: cmd.dry_run,
216 allow_dirty: cmd.allow_dirty,
217 force: cmd.force,
218 include_prereqs: cmd.include_prereqs,
219 target: cmd.target,
220 service: cmd.service,
221 manifest_path: cmd.manifest_path,
222 expected_version: None,
223 };
224
225 if let Err(e) = run_publish_command(options).await {
226 let _ = log_error("publish", "Publish command failed", Some(&e)).await;
227 return Err(ErrorFactory::operation(
228 "publish",
229 "run publish workflow",
230 e,
231 Some("Configure project targets with `xbp config npm setup-release` or `xbp config crates setup-release`."),
232 ));
233 }
234
235 Ok(())
236}
237
238pub(super) async fn handle_setup(debug: bool) -> CliResult<()> {
239 if let Err(e) = ui::with_loader("Running setup checks", run_setup(debug)).await {
240 let _ = log_error("setup", "Setup failed", Some(&e)).await;
241 return Err(ErrorFactory::operation(
242 "setup",
243 "setup environment",
244 e,
245 Some("Run with `--debug` for command-level output."),
246 ));
247 }
248 Ok(())
249}
250
251pub(super) async fn handle_redeploy(service_name: Option<String>, debug: bool) -> CliResult<()> {
252 if let Some(name) = service_name {
253 if let Err(e) = ui::with_loader(
254 &format!("Redeploying service `{}`", name),
255 run_redeploy_service(&name, debug),
256 )
257 .await
258 {
259 let _ = log_error("redeploy", "Service redeploy failed", Some(&e)).await;
260 return Err(ErrorFactory::operation(
261 "redeploy",
262 &format!("redeploy service `{}`", name),
263 e,
264 Some("Verify service name with `xbp services`."),
265 ));
266 }
267 } else if let Err(e) = ui::with_loader("Redeploying full project", run_redeploy()).await {
268 let _ = log_error("redeploy", "Redeploy failed", Some(&e)).await;
269 return Err(ErrorFactory::operation(
270 "redeploy",
271 "redeploy project",
272 e,
273 Some("Try `xbp redeploy <service>` for scoped retries."),
274 ));
275 }
276 Ok(())
277}
278
279pub(super) async fn handle_redeploy_v2(cmd: commands::RedeployV2Cmd, debug: bool) -> CliResult<()> {
280 let _ = log_info("redeploy_v2", "Starting remote redeploy process", None).await;
281 match run_redeploy_v2(cmd.password, cmd.username, cmd.host, cmd.project_dir, debug).await {
282 Ok(()) => Ok(()),
283 Err(e) => {
284 let _ = log_error("redeploy_v2", "Remote redeploy failed", Some(&e)).await;
285 Err(e.into())
286 }
287 }
288}
289
290pub(super) async fn handle_config(cmd: commands::ConfigCmd, debug: bool) -> CliResult<()> {
291 let commands::ConfigCmd {
292 project,
293 no_open,
294 provider,
295 } = cmd;
296
297 if provider.is_some() && (project || no_open) {
298 return Err(ErrorFactory::validation(
299 "config",
300 "`xbp config <provider> ...` cannot be combined with `--project` or `--no-open`.",
301 Some("Run either provider key management OR project/global config actions."),
302 ));
303 }
304
305 if let Some(provider_cmd) = provider {
306 match provider_cmd {
307 commands::ConfigProviderCmd::Openrouter(subcmd) => match subcmd.action {
308 commands::ConfigSecretAction::SetKey { key } => {
309 if let Err(e) = run_config_secret_set("openrouter", key).await {
310 let _ = log_error("config", "Failed to set OpenRouter key", Some(&e)).await;
311 return Err(ErrorFactory::operation(
312 "config",
313 "set OpenRouter key",
314 e,
315 Some("Run `xbp config openrouter show` to confirm key state."),
316 ));
317 }
318 }
319 commands::ConfigSecretAction::DeleteKey => {
320 if let Err(e) = run_config_secret_delete("openrouter").await {
321 let _ =
322 log_error("config", "Failed to delete OpenRouter key", Some(&e)).await;
323 return Err(ErrorFactory::operation(
324 "config",
325 "delete OpenRouter key",
326 e,
327 Some("Use `xbp config openrouter show` to verify removal."),
328 ));
329 }
330 }
331 commands::ConfigSecretAction::Show { raw } => {
332 if let Err(e) = run_config_secret_show("openrouter", raw).await {
333 let _ =
334 log_error("config", "Failed to show OpenRouter key", Some(&e)).await;
335 return Err(ErrorFactory::operation(
336 "config",
337 "show OpenRouter key",
338 e,
339 None,
340 ));
341 }
342 }
343 },
344 commands::ConfigProviderCmd::Github(subcmd) => match subcmd.action {
345 commands::ConfigSecretAction::SetKey { key } => {
346 if let Err(e) = run_config_secret_set("github", key).await {
347 let _ = log_error("config", "Failed to set GitHub token", Some(&e)).await;
348 return Err(ErrorFactory::operation(
349 "config",
350 "set GitHub token",
351 e,
352 Some("Use a token with repo scope for private repos."),
353 ));
354 }
355 }
356 commands::ConfigSecretAction::DeleteKey => {
357 if let Err(e) = run_config_secret_delete("github").await {
358 let _ =
359 log_error("config", "Failed to delete GitHub token", Some(&e)).await;
360 return Err(ErrorFactory::operation(
361 "config",
362 "delete GitHub token",
363 e,
364 None,
365 ));
366 }
367 }
368 commands::ConfigSecretAction::Show { raw } => {
369 if let Err(e) = run_config_secret_show("github", raw).await {
370 let _ = log_error("config", "Failed to show GitHub token", Some(&e)).await;
371 return Err(ErrorFactory::operation(
372 "config",
373 "show GitHub token",
374 e,
375 None,
376 ));
377 }
378 }
379 },
380 commands::ConfigProviderCmd::Cloudflare(subcmd) => match subcmd.action {
381 None => {
382 if let Err(e) = run_config_cloudflare_setup().await {
383 let _ =
384 log_error("config", "Failed to configure Cloudflare", Some(&e)).await;
385 return Err(ErrorFactory::operation(
386 "config",
387 "configure Cloudflare",
388 e,
389 Some(
390 "Run `xbp config cloudflare status` to inspect credential sources.",
391 ),
392 ));
393 }
394 }
395 Some(commands::CloudflareConfigAction::SetKey { key }) => {
396 if let Err(e) = run_config_secret_set("cloudflare", key).await {
397 let _ =
398 log_error("config", "Failed to set Cloudflare token", Some(&e)).await;
399 return Err(ErrorFactory::operation(
400 "config",
401 "set Cloudflare token",
402 e,
403 Some("Use a Cloudflare API token with Secrets Store and DNS permissions."),
404 ));
405 }
406 }
407 Some(commands::CloudflareConfigAction::DeleteKey) => {
408 if let Err(e) = run_config_secret_delete("cloudflare").await {
409 let _ = log_error("config", "Failed to delete Cloudflare token", Some(&e))
410 .await;
411 return Err(ErrorFactory::operation(
412 "config",
413 "delete Cloudflare token",
414 e,
415 None,
416 ));
417 }
418 }
419 Some(commands::CloudflareConfigAction::ShowKey { raw }) => {
420 if let Err(e) = run_config_secret_show("cloudflare", raw).await {
421 let _ =
422 log_error("config", "Failed to show Cloudflare token", Some(&e)).await;
423 return Err(ErrorFactory::operation(
424 "config",
425 "show Cloudflare token",
426 e,
427 None,
428 ));
429 }
430 }
431 Some(commands::CloudflareConfigAction::SetAccountId { account_id }) => {
432 if let Err(e) = run_config_cloudflare_account_set(account_id).await {
433 let _ =
434 log_error("config", "Failed to set Cloudflare account ID", Some(&e))
435 .await;
436 return Err(ErrorFactory::operation(
437 "config",
438 "set Cloudflare account ID",
439 e,
440 None,
441 ));
442 }
443 }
444 Some(commands::CloudflareConfigAction::DeleteAccountId) => {
445 if let Err(e) = run_config_cloudflare_account_delete().await {
446 let _ =
447 log_error("config", "Failed to delete Cloudflare account ID", Some(&e))
448 .await;
449 return Err(ErrorFactory::operation(
450 "config",
451 "delete Cloudflare account ID",
452 e,
453 None,
454 ));
455 }
456 }
457 Some(commands::CloudflareConfigAction::ShowAccountId { raw }) => {
458 if let Err(e) = run_config_cloudflare_account_show(raw).await {
459 let _ =
460 log_error("config", "Failed to show Cloudflare account ID", Some(&e))
461 .await;
462 return Err(ErrorFactory::operation(
463 "config",
464 "show Cloudflare account ID",
465 e,
466 None,
467 ));
468 }
469 }
470 Some(commands::CloudflareConfigAction::Login) => {
471 if let Err(e) = run_config_cloudflare_login().await {
472 let _ = log_error("config", "Failed to link Cloudflare", Some(&e)).await;
473 return Err(ErrorFactory::operation(
474 "config",
475 "link Cloudflare",
476 e,
477 None,
478 ));
479 }
480 }
481 Some(commands::CloudflareConfigAction::Status) => {
482 if let Err(e) = run_config_cloudflare_status().await {
483 let _ =
484 log_error("config", "Failed to show Cloudflare status", Some(&e)).await;
485 return Err(ErrorFactory::operation(
486 "config",
487 "show Cloudflare status",
488 e,
489 None,
490 ));
491 }
492 }
493 Some(commands::CloudflareConfigAction::Setup) => {
494 if let Err(e) = run_config_cloudflare_setup().await {
495 let _ =
496 log_error("config", "Failed to configure Cloudflare", Some(&e)).await;
497 return Err(ErrorFactory::operation(
498 "config",
499 "configure Cloudflare",
500 e,
501 None,
502 ));
503 }
504 }
505 },
506 commands::ConfigProviderCmd::Linear(subcmd) => match subcmd.action {
507 commands::LinearConfigAction::SetKey { key } => {
508 if let Err(e) = run_config_secret_set("linear", key).await {
509 let _ = log_error("config", "Failed to set Linear API key", Some(&e)).await;
510 return Err(ErrorFactory::operation(
511 "config",
512 "set Linear API key",
513 e,
514 Some("Use this to link Linear issue IDs in generated release notes and publish release updates to Linear initiatives."),
515 ));
516 }
517 }
518 commands::LinearConfigAction::DeleteKey => {
519 if let Err(e) = run_config_secret_delete("linear").await {
520 let _ =
521 log_error("config", "Failed to delete Linear API key", Some(&e)).await;
522 return Err(ErrorFactory::operation(
523 "config",
524 "delete Linear API key",
525 e,
526 None,
527 ));
528 }
529 }
530 commands::LinearConfigAction::Show { raw } => {
531 if let Err(e) = run_config_secret_show("linear", raw).await {
532 let _ =
533 log_error("config", "Failed to show Linear API key", Some(&e)).await;
534 return Err(ErrorFactory::operation(
535 "config",
536 "show Linear API key",
537 e,
538 None,
539 ));
540 }
541 }
542 commands::LinearConfigAction::SelectInitiative => {
543 if let Err(e) = run_config_linear_select_initiative().await {
544 let _ = log_error(
545 "config",
546 "Failed to select repo Linear initiative",
547 Some(&e),
548 )
549 .await;
550 return Err(ErrorFactory::operation(
551 "config",
552 "select repo Linear initiative",
553 e,
554 Some("Run this inside an XBP project and configure a Linear key with `xbp config linear set-key` first."),
555 ));
556 }
557 }
558 },
559 commands::ConfigProviderCmd::Npm(subcmd) => match subcmd.action {
560 commands::RegistryConfigAction::SetKey { key } => {
561 if let Err(e) = run_config_secret_set("npm", key).await {
562 let _ = log_error("config", "Failed to set npm token", Some(&e)).await;
563 return Err(ErrorFactory::operation(
564 "config",
565 "set npm token",
566 e,
567 Some("Use a valid npm automation or granular publish token."),
568 ));
569 }
570 }
571 commands::RegistryConfigAction::DeleteKey => {
572 if let Err(e) = run_config_secret_delete("npm").await {
573 let _ = log_error("config", "Failed to delete npm token", Some(&e)).await;
574 return Err(ErrorFactory::operation(
575 "config",
576 "delete npm token",
577 e,
578 None,
579 ));
580 }
581 }
582 commands::RegistryConfigAction::Show { raw } => {
583 if let Err(e) = run_config_secret_show("npm", raw).await {
584 let _ = log_error("config", "Failed to show npm token", Some(&e)).await;
585 return Err(ErrorFactory::operation("config", "show npm token", e, None));
586 }
587 }
588 commands::RegistryConfigAction::SetupRelease => {
589 if let Err(e) = run_config_publish_setup("npm").await {
590 let _ =
591 log_error("config", "Failed to configure npm publish", Some(&e)).await;
592 return Err(ErrorFactory::operation(
593 "config",
594 "configure npm publish",
595 e,
596 Some("Run this inside the target XBP project and ensure the package manifest exists."),
597 ));
598 }
599 }
600 },
601 commands::ConfigProviderCmd::Crates(subcmd) => match subcmd.action {
602 commands::CratesConfigAction::SetKey { key } => {
603 if let Err(e) = run_config_secret_set("crates", key).await {
604 let _ = log_error("config", "Failed to set crates token", Some(&e)).await;
605 return Err(ErrorFactory::operation(
606 "config",
607 "set crates token",
608 e,
609 Some("Use a crates.io API token or rely on CARGO_REGISTRY_TOKEN. Then run `xbp config crates login` if you also want Cargo's local credentials file updated."),
610 ));
611 }
612 }
613 commands::CratesConfigAction::DeleteKey => {
614 if let Err(e) = run_config_secret_delete("crates").await {
615 let _ =
616 log_error("config", "Failed to delete crates token", Some(&e)).await;
617 return Err(ErrorFactory::operation(
618 "config",
619 "delete crates token",
620 e,
621 None,
622 ));
623 }
624 }
625 commands::CratesConfigAction::Show { raw } => {
626 if let Err(e) = run_config_secret_show("crates", raw).await {
627 let _ = log_error("config", "Failed to show crates token", Some(&e)).await;
628 return Err(ErrorFactory::operation(
629 "config",
630 "show crates token",
631 e,
632 None,
633 ));
634 }
635 }
636 commands::CratesConfigAction::SetupRelease => {
637 if let Err(e) = run_config_publish_setup("crates").await {
638 let _ = log_error("config", "Failed to configure crates publish", Some(&e))
639 .await;
640 return Err(ErrorFactory::operation(
641 "config",
642 "configure crates publish",
643 e,
644 Some("Run this inside the target XBP project and point the workflow at the crate manifest you want to publish."),
645 ));
646 }
647 }
648 commands::CratesConfigAction::Login { key } => {
649 if let Err(e) = run_config_crates_login(key).await {
650 let _ = log_error("config", "Failed to log Cargo into crates.io", Some(&e))
651 .await;
652 return Err(ErrorFactory::operation(
653 "config",
654 "log Cargo into crates.io",
655 e,
656 Some("Store a crates token with `xbp config crates set-key` first, or pass it directly to `xbp config crates login <token>`."),
657 ));
658 }
659 }
660 commands::CratesConfigAction::Logout => {
661 if let Err(e) = run_config_crates_logout().await {
662 let _ =
663 log_error("config", "Failed to log Cargo out of crates.io", Some(&e))
664 .await;
665 return Err(ErrorFactory::operation(
666 "config",
667 "log Cargo out of crates.io",
668 e,
669 Some("This only clears Cargo's local credentials. Use `xbp config crates delete-key` if you also want to remove the token from global XBP config."),
670 ));
671 }
672 }
673 },
674 commands::ConfigProviderCmd::Release(subcmd) => match subcmd.action {
675 commands::ReleaseConfigAction::Setup => {
676 if let Err(e) = run_config_release_setup().await {
677 let _ =
678 log_error("config", "Failed to configure release settings", Some(&e))
679 .await;
680 return Err(ErrorFactory::operation(
681 "config",
682 "configure release settings",
683 e,
684 Some("Run this inside the target XBP project to update .xbp/xbp.yaml."),
685 ));
686 }
687 }
688 },
689 commands::ConfigProviderCmd::Openapi(subcmd) => match subcmd.action {
690 None | Some(commands::OpenapiConfigAction::Setup) => {
691 if let Err(e) = run_config_openapi_setup().await {
692 let _ =
693 log_error("config", "Failed to configure OpenAPI generation", Some(&e))
694 .await;
695 return Err(ErrorFactory::operation(
696 "config",
697 "configure OpenAPI generation",
698 e,
699 Some("Run this inside the target XBP project to update .xbp/xbp.yaml."),
700 ));
701 }
702 }
703 Some(commands::OpenapiConfigAction::Show) => {
704 if let Err(e) = run_config_openapi_show().await {
705 let _ =
706 log_error("config", "Failed to show OpenAPI config", Some(&e)).await;
707 return Err(ErrorFactory::operation(
708 "config",
709 "show OpenAPI config",
710 e,
711 Some("Run this inside the target XBP project."),
712 ));
713 }
714 }
715 },
716 }
717 } else if project {
718 if let Err(e) = run_config(debug).await {
719 return Err(ErrorFactory::operation(
720 "config",
721 "read project config",
722 e,
723 Some("Ensure you're inside an XBP project root."),
724 ));
725 }
726 } else if let Err(e) = open_global_config(no_open).await {
727 let _ = log_error("config", "Failed to open global config", Some(&e)).await;
728 return Err(ErrorFactory::operation(
729 "config",
730 "open global config",
731 e,
732 None,
733 ));
734 }
735 Ok(())
736}
737
738pub(super) async fn handle_install(
739 package: Option<String>,
740 list: bool,
741 force: bool,
742 debug: bool,
743) -> CliResult<()> {
744 if list {
745 crate::commands::print_install_targets_help();
746 return Ok(());
747 }
748
749 let Some(package) = package else {
750 crate::commands::print_install_empty_state();
751 return Ok(());
752 };
753
754 if package.trim().is_empty() {
755 crate::commands::print_install_empty_state();
756 return Ok(());
757 }
758
759 if crate::commands::is_install_listing_request(&package) {
760 crate::commands::print_install_targets_help();
761 return Ok(());
762 }
763
764 let install_msg: String = format!("Installing package: {}", package);
765 let _ = log_info("install", &install_msg, None).await;
766 match ui::with_loader(
767 &format!("Installing package `{}`", package),
768 install_package(&package, force, debug),
769 )
770 .await
771 {
772 Ok(()) => {
773 let success_msg = format!("Successfully installed: {}", package);
774 let _ = log_success("install", &success_msg, None).await;
775 Ok(())
776 }
777 Err(e) => Err(e.into()),
778 }
779}
780
781pub(super) async fn handle_curl(cmd: commands::CurlCmd, debug: bool) -> CliResult<()> {
782 let url = cmd
783 .url
784 .unwrap_or_else(|| "https://example.com/api".to_string());
785 if let Err(e) = ui::with_loader(
786 &format!("Requesting {}", url),
787 curl::run_curl(&url, cmd.no_timeout, debug),
788 )
789 .await
790 {
791 let _ = log_error("curl", "Curl command failed", Some(&e)).await;
792 return Err(ErrorFactory::operation(
793 "curl",
794 "execute request",
795 e,
796 Some("Double-check the URL and network connectivity."),
797 ));
798 }
799 Ok(())
800}
801
802pub(super) async fn handle_services(debug: bool) -> CliResult<()> {
803 if let Err(e) = ui::with_loader("Loading configured services", list_services(debug)).await {
804 let _ = log_error("services", "Failed to list services", Some(&e)).await;
805 return Err(ErrorFactory::operation(
806 "services",
807 "list services",
808 e,
809 Some("Ensure xbp config is present and valid."),
810 ));
811 }
812 Ok(())
813}
814
815pub(super) async fn handle_service(
816 command: Option<String>,
817 service_name: Option<String>,
818 debug: bool,
819) -> CliResult<()> {
820 match (command.as_deref(), service_name.as_deref()) {
821 (None, None) => {
823 if let Err(e) = run_service_interactive(debug).await {
824 let _ = log_error("service", "Interactive service run failed", Some(&e)).await;
825 return Err(ErrorFactory::operation(
826 "service",
827 "interactive picker",
828 e,
829 None,
830 ));
831 }
832 }
833 (Some(cmd), None) if cmd != "help" && cmd != "--help" => {
835 if let Err(e) = pick_service_for_command(cmd, debug).await {
836 let _ = log_error(
837 "service",
838 &format!("Service pick for '{}' failed", cmd),
839 Some(&e),
840 )
841 .await;
842 return Err(ErrorFactory::operation(
843 "service",
844 &format!("pick service for {}", cmd),
845 e,
846 Some("Pass a service name explicitly, or ensure commands are defined in xbp config."),
847 ));
848 }
849 }
850 (Some(cmd), Some(name)) if cmd == "help" || cmd == "--help" => {
852 if let Err(e) = show_service_help(name).await {
853 let _ = log_error("service", "Failed to show service help", Some(&e)).await;
854 return Err(ErrorFactory::operation(
855 "service",
856 "show service help",
857 e,
858 None,
859 ));
860 }
861 }
862 (Some(cmd), Some(name)) => {
864 if let Err(e) = run_service_command(cmd, name, debug).await {
865 let _ = log_error(
866 "service",
867 &format!("Service command '{}' failed", cmd),
868 Some(&e),
869 )
870 .await;
871 return Err(ErrorFactory::operation(
872 "service",
873 &format!("run `{}` for `{}`", cmd, name),
874 e,
875 Some("Check available services via `xbp services`."),
876 ));
877 }
878 }
879 (None, Some(name)) => {
881 if let Err(e) = show_service_help(name).await {
882 let _ = log_warn(
884 "service",
885 &format!("Could not show help for service '{}': {}", name, e),
886 None,
887 )
888 .await;
889 print_service_usage();
890 }
891 }
892 (Some(_), None) => {
894 print_service_usage();
895 }
896 }
897 Ok(())
898}
899
900pub(super) async fn handle_nginx(cmd: commands::NginxSubCommand, debug: bool) -> CliResult<()> {
901 if let Err(e) = run_nginx(cmd, debug).await {
902 let _ = log_error("nginx", "Nginx command failed", Some(&e.to_string())).await;
903 return Err(ErrorFactory::operation(
904 "nginx",
905 "execute nginx command",
906 e.to_string(),
907 Some("Try `xbp nginx --help` for command syntax."),
908 ));
909 }
910 Ok(())
911}
912
913pub(super) async fn handle_diag(cmd: commands::DiagCmd, debug: bool) -> CliResult<()> {
914 if let Err(e) = ui::with_loader("Running diagnostics", run_diag(cmd, debug)).await {
915 let _ = log_error("diag", "Diag command failed", Some(&e.to_string())).await;
916 return Err(ErrorFactory::operation(
917 "diag",
918 "run diagnostics",
919 e.to_string(),
920 Some("Re-run with `--debug` for more context."),
921 ));
922 }
923 Ok(())
924}
925
926pub(super) async fn handle_generate(cmd: commands::GenerateCmd, debug: bool) -> CliResult<()> {
927 match cmd.command {
928 commands::GenerateSubCommand::Config(subcmd) => {
929 let args = GenerateConfigArgs {
930 force: subcmd.force,
931 update: subcmd.update,
932 from_json: subcmd.from_json,
933 };
934 if let Err(e) = run_generate_config(args, debug).await {
935 let _ = log_error(
936 "generate-config",
937 "Failed to generate project config",
938 Some(&e),
939 )
940 .await;
941 return Err(ErrorFactory::operation(
942 "generate-config",
943 "generate .xbp/xbp.yaml",
944 e,
945 Some("Use --update to refresh an existing config or --force to overwrite it."),
946 ));
947 }
948 }
949 #[cfg(feature = "openapi-gen")]
950 commands::GenerateSubCommand::Openapi(subcmd) => {
951 let args = GenerateOpenApiArgs {
952 services: subcmd.service,
953 all: subcmd.all,
954 aggregate: false,
955 format: subcmd.format,
956 output: subcmd.output,
957 check: subcmd.check,
958 dry_run: subcmd.dry_run,
959 strict: subcmd.strict,
960 no_cache: subcmd.no_cache,
961 version: None,
962 };
963 if let Err(e) = run_generate_openapi(args, debug).await {
964 let _ = log_error("generate-openapi", "Failed to generate OpenAPI", Some(&e)).await;
965 return Err(ErrorFactory::operation(
966 "generate-openapi",
967 "generate OpenAPI documents",
968 e,
969 Some("Check openapi settings in .xbp/xbp.yaml or use --strict for unresolved type details."),
970 ));
971 }
972 }
973 commands::GenerateSubCommand::Systemd(subcmd) => {
974 let args = GenerateSystemdArgs {
975 output_dir: subcmd.output_dir,
976 service: subcmd.service,
977 api: subcmd.api,
978 };
979 if let Err(e) = run_generate_systemd(args, debug).await {
980 let _ = log_error(
981 "generate-systemd",
982 "Failed to generate systemd units",
983 Some(&e),
984 )
985 .await;
986 return Err(ErrorFactory::operation(
987 "generate-systemd",
988 "generate unit files",
989 e,
990 Some("Use a writable `--output-dir` or run with elevated permissions."),
991 ));
992 }
993 }
994 }
995 Ok(())
996}
997
998pub(super) async fn handle_done(cmd: commands::DoneCmd, _debug: bool) -> CliResult<()> {
999 if let Err(e) = crate::commands::run_done(
1000 cmd.root,
1001 cmd.since,
1002 cmd.output,
1003 cmd.no_ai,
1004 cmd.recursive,
1005 cmd.exclude,
1006 )
1007 .await
1008 {
1009 let _ = log_error("done", "Done command failed", Some(&e)).await;
1010 return Err(e.into());
1011 }
1012 Ok(())
1013}
1014
1015pub(super) async fn handle_fix_process_monitor_json(
1016 cmd: commands::FixProcessMonitorJsonCmd,
1017 _debug: bool,
1018) -> CliResult<()> {
1019 if let Err(error) =
1020 crate::commands::run_fix_process_monitor_json(cmd.path, cmd.check, cmd.stdout)
1021 {
1022 let _ = log_error(
1023 "fix-process-monitor-json",
1024 "Failed to repair process monitor JSON",
1025 Some(&error),
1026 )
1027 .await;
1028 return Err(error.into());
1029 }
1030 Ok(())
1031}
1032
1033pub(super) async fn handle_cursor(cmd: commands::CursorCmd) -> CliResult<()> {
1034 let result = match cmd.command {
1035 commands::CursorSubCommand::Ingest { dry_run } => run_cursor_ingest(dry_run).await,
1036 };
1037
1038 if let Err(error) = result {
1039 let _ = log_error("cursor", "Cursor command failed", Some(&error)).await;
1040 return Err(error.into());
1041 }
1042 Ok(())
1043}
1044
1045pub(super) async fn handle_login(cmd: commands::LoginCmd) -> CliResult<()> {
1046 let result = match cmd.action {
1047 Some(commands::LoginSubCommand::Status) => run_login_status().await,
1048 Some(commands::LoginSubCommand::Logout) => run_logout().await,
1049 None => run_login().await,
1050 };
1051
1052 if let Err(e) = result {
1053 let _ = log_error("login", "Login command failed", Some(&e)).await;
1054 return Err(e.into());
1055 }
1056
1057 Ok(())
1058}
1059
1060pub(super) async fn handle_whoami() -> CliResult<()> {
1061 if let Err(e) = run_whoami().await {
1062 let _ = log_error("whoami", "Whoami command failed", Some(&e)).await;
1063 return Err(e.into());
1064 }
1065
1066 Ok(())
1067}
1068
1069pub(super) async fn handle_version(cmd: commands::VersionCmd, debug: bool) -> CliResult<()> {
1070 if let Err(e) = require_authenticated_cli_session().await {
1071 return Err(ErrorFactory::operation(
1072 "version",
1073 "verify CLI login",
1074 e,
1075 Some("Run `xbp login` before using version workflows."),
1076 ));
1077 }
1078
1079 let commands::VersionCmd {
1080 target,
1081 explicit_version,
1082 git,
1083 command,
1084 } = cmd;
1085 let resolved_target = explicit_version.or(target);
1086
1087 if command.is_some() && (resolved_target.is_some() || git) {
1088 return Err(ErrorFactory::validation(
1089 "version",
1090 "`xbp version release` cannot be combined with `--git`, positional targets, or `--version`/`-v` on the parent command.",
1091 Some(
1092 "Run `xbp version release` as a standalone command, or use `xbp version --version <x.y.z>` without a subcommand.",
1093 ),
1094 ));
1095 }
1096
1097 if let Some(subcommand) = command {
1098 match subcommand {
1099 commands::VersionSubCommand::Release(release_cmd) => {
1100 let options = VersionReleaseOptions {
1101 explicit_version: release_cmd.version,
1102 release_flag: release_cmd.flag.map(|flag| flag.as_str().to_string()),
1103 allow_dirty: release_cmd.allow_dirty,
1104 title: release_cmd.title,
1105 notes: release_cmd.notes,
1106 notes_file: release_cmd.notes_file,
1107 draft: release_cmd.draft,
1108 prerelease: release_cmd.prerelease,
1109 publish: release_cmd.publish,
1110 force: release_cmd.force,
1111 latest_policy: match release_cmd.make_latest {
1112 commands::VersionReleaseLatest::True => ReleaseLatestPolicy::True,
1113 commands::VersionReleaseLatest::False => ReleaseLatestPolicy::False,
1114 commands::VersionReleaseLatest::Legacy => ReleaseLatestPolicy::Legacy,
1115 },
1116 };
1117 if let Err(e) = run_version_release_command(options).await {
1118 let hint = version_release_error_hint(&e);
1119 return Err(ErrorFactory::operation(
1120 "version",
1121 "release version",
1122 e,
1123 hint,
1124 ));
1125 }
1126 return Ok(());
1127 }
1128 commands::VersionSubCommand::Discover(discover_cmd) => {
1129 if let Err(e) = run_version_discover_services(&discover_cmd).await {
1130 return Err(ErrorFactory::operation(
1131 "version",
1132 "discover and register project services",
1133 e,
1134 Some(
1135 "Run `xbp version discover --dry-run` to preview package roots, or `--no-register` to opt out of writing.",
1136 ),
1137 ));
1138 }
1139 return Ok(());
1140 }
1141 commands::VersionSubCommand::Bump(bump_cmd) => {
1142 if let Err(e) = run_version_bump_command(&bump_cmd).await {
1143 return Err(ErrorFactory::operation(
1144 "version",
1145 "bump mutated package versions",
1146 e,
1147 Some(
1148 "Run `xbp version bump --dry-run` to preview candidates. On a clean tree every registered package is offered; use `--all --patch` (or `--minor`/`--major`) in non-interactive shells.",
1149 ),
1150 ));
1151 }
1152 return Ok(());
1153 }
1154 commands::VersionSubCommand::Domain(domain_cmd) => {
1155 let command = match domain_cmd.command {
1156 commands::VersionDomainSubCommand::Init(init_cmd) => {
1157 VersionDomainCommand::Init(VersionDomainInitOptions {
1158 name: init_cmd.name,
1159 root: init_cmd.root,
1160 write: init_cmd.write,
1161 })
1162 }
1163 commands::VersionDomainSubCommand::Doctor(doctor_cmd) => {
1164 VersionDomainCommand::Doctor(VersionDomainDoctorOptions {
1165 domain: doctor_cmd.domain,
1166 })
1167 }
1168 commands::VersionDomainSubCommand::Sync(sync_cmd) => {
1169 VersionDomainCommand::Sync(VersionDomainSyncOptions {
1170 domain: sync_cmd.domain,
1171 write: sync_cmd.write,
1172 })
1173 }
1174 commands::VersionDomainSubCommand::Diagnose(diagnose_cmd) => {
1175 VersionDomainCommand::Diagnose(VersionDomainDiagnoseOptions {
1176 domain: diagnose_cmd.domain,
1177 log: diagnose_cmd.log,
1178 })
1179 }
1180 commands::VersionDomainSubCommand::Release(release_cmd) => {
1181 VersionDomainCommand::Release(VersionDomainReleaseOptions {
1182 domain: release_cmd.domain,
1183 patch: release_cmd.patch,
1184 minor: release_cmd.minor,
1185 major: release_cmd.major,
1186 version: release_cmd.version,
1187 deploy: release_cmd.deploy,
1188 allow_major_jump: release_cmd.allow_major_jump,
1189 allow_cross_domain_version: release_cmd.allow_cross_domain_version,
1190 })
1191 }
1192 };
1193 if let Err(e) =
1194 run_version_domain_command(VersionDomainCommandOptions { command }).await
1195 {
1196 return Err(ErrorFactory::operation(
1197 "version",
1198 "run version-domain command",
1199 e,
1200 Some("Run `xbp version domain -h` to inspect supported usage."),
1201 ));
1202 }
1203 return Ok(());
1204 }
1205 commands::VersionSubCommand::Workspace(workspace_cmd) => {
1206 let (repo, json, workspace_command) = match workspace_cmd.command {
1207 commands::VersionWorkspaceSubCommand::Check(check_cmd) => (
1208 check_cmd.target.repo,
1209 check_cmd.target.json,
1210 WorkspaceVersionCommand::Check(WorkspaceVersionCheckOptions {
1211 version: check_cmd.version,
1212 }),
1213 ),
1214 commands::VersionWorkspaceSubCommand::Sync(sync_cmd) => (
1215 sync_cmd.target.repo,
1216 sync_cmd.target.json,
1217 WorkspaceVersionCommand::Sync(WorkspaceVersionSyncOptions {
1218 version: sync_cmd.version,
1219 write: sync_cmd.write,
1220 }),
1221 ),
1222 commands::VersionWorkspaceSubCommand::Validate(validate_cmd) => (
1223 validate_cmd.target.repo,
1224 validate_cmd.target.json,
1225 WorkspaceVersionCommand::Validate(WorkspaceVersionValidateOptions {
1226 package: validate_cmd.package,
1227 cargo_check: validate_cmd.cargo_check,
1228 package_dry_run: validate_cmd.package_dry_run,
1229 }),
1230 ),
1231 commands::VersionWorkspaceSubCommand::Publish(publish_cmd) => {
1232 match publish_cmd.command {
1233 commands::VersionWorkspacePublishSubCommand::Plan(plan_cmd) => (
1234 plan_cmd.target.repo,
1235 plan_cmd.target.json,
1236 WorkspaceVersionCommand::PublishPlan(WorkspacePublishPlanOptions {
1237 only: plan_cmd.only,
1238 include_prereqs: plan_cmd.include_prereqs,
1239 }),
1240 ),
1241 commands::VersionWorkspacePublishSubCommand::Run(run_cmd) => (
1242 run_cmd.target.repo,
1243 run_cmd.target.json,
1244 WorkspaceVersionCommand::PublishRun(WorkspacePublishRunOptions {
1245 dry_run: run_cmd.dry_run,
1246 from: run_cmd.from,
1247 only: run_cmd.only,
1248 include_prereqs: run_cmd.include_prereqs,
1249 continue_on_error: run_cmd.continue_on_error,
1250 allow_dirty: run_cmd.allow_dirty,
1251 timeout_seconds: run_cmd.timeout_seconds,
1252 poll_interval_seconds: run_cmd.poll_interval_seconds,
1253 }),
1254 ),
1255 }
1256 }
1257 };
1258
1259 if let Err(e) = run_version_workspace_command(WorkspaceVersionCommandOptions {
1260 repo,
1261 json,
1262 command: workspace_command,
1263 })
1264 .await
1265 {
1266 return Err(ErrorFactory::operation(
1267 "version",
1268 "run workspace version command",
1269 e,
1270 Some("Run `xbp version workspace -h` to inspect supported usage."),
1271 ));
1272 }
1273 return Ok(());
1274 }
1275 }
1276 }
1277
1278 if let Err(e) = run_version_command(resolved_target, git, debug).await {
1279 return Err(ErrorFactory::operation(
1280 "version",
1281 "run version command",
1282 e,
1283 Some("Run `xbp version -h` to inspect supported usage."),
1284 ));
1285 }
1286 Ok(())
1287}
1288
1289fn version_release_error_hint(error: &str) -> Option<&'static str> {
1290 if error.contains("Working tree is dirty") || error.contains("uncommitted changes") {
1291 return Some(
1292 "Use `--allow-dirty` if only generated files changed. `--force` also allows a dirty tree when `--publish` is enabled.",
1293 );
1294 }
1295 if error.contains("CARGO_REGISTRY_TOKEN") || error.contains("no token found") {
1296 return Some(
1297 "Configure a crates.io token with `xbp config crates set-key` or export `CARGO_REGISTRY_TOKEN`.",
1298 );
1299 }
1300 if error.contains("Workspace version drift detected") {
1301 return Some(
1302 "Run `xbp version workspace sync --write` to align crate versions, or pass `--force` to auto-sync during release.",
1303 );
1304 }
1305 if error.contains("Workspace closure detected")
1306 || error.contains("workspace publish command failed")
1307 {
1308 return Some(
1309 "For multi-crate workspaces, inspect the publish output and run `xbp version workspace publish plan --include-prereqs` first.",
1310 );
1311 }
1312 if error.contains("Duplicate port found") || error.contains("Duplicate service name") {
1313 return Some(
1314 "Duplicate service ports/names are deploy-time checks. Re-run `xbp version release` (release no longer blocks on them). Fix ports in `.xbp/xbp.yaml` before `xbp deploy`/`xbp start`.",
1315 );
1316 }
1317 if error.contains("Release cancelled") {
1318 return Some(
1319 "Re-run with `--allow-dirty` to skip dirty-tree prompts, or `--force` to auto-heal workspace versions and allow dirty publishes.",
1320 );
1321 }
1322 None
1323}
1324
1325fn should_print_help(cli: &Cli) -> bool {
1326 cli.command.is_none() && !cli.list && !cli.logs && !cli.commands && cli.port.is_none()
1327}
1328
1329fn background_cursor_ingest_trigger(command: Option<&commands::Commands>) -> Option<&'static str> {
1330 match command {
1331 None => None,
1332 Some(commands::Commands::Cursor(_)) => None,
1333 Some(commands::Commands::Diag(_)) => None,
1334 Some(commands::Commands::Whoami) => None,
1335 Some(commands::Commands::Login(login_cmd)) => match login_cmd.action {
1336 None => Some("login"),
1337 Some(commands::LoginSubCommand::Status | commands::LoginSubCommand::Logout) => None,
1338 },
1339 Some(other) => Some(commands::command_label(other)),
1340 }
1341}
1342
1343fn background_system_inventory_refresh_trigger(
1344 command: Option<&commands::Commands>,
1345) -> Option<&'static str> {
1346 match command {
1347 None => None,
1348 Some(commands::Commands::Cursor(_)) => None,
1349 Some(commands::Commands::Diag(_)) => None,
1350 Some(commands::Commands::Whoami) => None,
1351 Some(commands::Commands::Login(login_cmd)) => match login_cmd.action {
1352 None => Some("login"),
1353 Some(commands::LoginSubCommand::Status | commands::LoginSubCommand::Logout) => None,
1354 },
1355 Some(other) => Some(commands::command_label(other)),
1356 }
1357}
1358
1359fn print_service_usage() {
1360 ui::configure_color_output();
1361 println!();
1362 println!("{}", "xbp service".bright_magenta().bold());
1363 ui::divider(28);
1364 println!(
1365 "{} {}",
1366 "Usage:".bright_cyan().bold(),
1367 "xbp service [command] [service-name]".bright_white()
1368 );
1369 println!();
1370 println!("Running with no arguments opens an interactive preview + picker.");
1371 ui::section("Commands");
1372 for command in ["build", "install", "start", "dev", "pre"] {
1373 println!(" {}", command.bright_cyan());
1374 }
1375 ui::section("Examples");
1376 println!(
1377 " {}",
1378 "xbp service # interactive: pick service → command".dimmed()
1379 );
1380 println!(
1381 " {}",
1382 "xbp service start # pick from services that support start".dimmed()
1383 );
1384 println!(" {}", "xbp service build zeus".dimmed());
1385 ui::tip("Run `xbp service --help <service-name>` for service-specific guidance. All runs are recorded under $HOME/.xbp/logs/service/");
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390 use super::*;
1391 use crate::cli::commands::{Commands, VersionReleaseLatest, VersionSubCommand};
1392
1393 #[test]
1394 fn plain_cli_invocation_prints_help() {
1395 let cli = Cli::try_parse_from(["xbp"]).expect("parse");
1396 assert!(should_print_help(&cli));
1397 }
1398
1399 #[test]
1400 fn root_version_short_v_displays_version() {
1401 let err = Cli::try_parse_from(["xbp", "-v"]).expect_err("version");
1402 assert_eq!(err.kind(), ClapErrorKind::DisplayVersion);
1403 }
1404
1405 #[test]
1406 fn root_version_capital_v_and_long_still_display_version() {
1407 let err_v = Cli::try_parse_from(["xbp", "-V"]).expect_err("version -V");
1408 assert_eq!(err_v.kind(), ClapErrorKind::DisplayVersion);
1409 let err_long = Cli::try_parse_from(["xbp", "--version"]).expect_err("version long");
1410 assert_eq!(err_long.kind(), ClapErrorKind::DisplayVersion);
1411 }
1412
1413 #[test]
1414 fn list_flag_skips_help_short_circuit() {
1415 let cli = Cli::try_parse_from(["xbp", "-l"]).expect("parse");
1416 assert!(!should_print_help(&cli));
1417 }
1418
1419 #[test]
1420 fn commands_flag_skips_help_short_circuit() {
1421 let cli = Cli::try_parse_from(["xbp", "--commands"]).expect("parse");
1422 assert!(!should_print_help(&cli));
1423 assert!(cli.commands);
1424 }
1425
1426 #[test]
1427 fn install_without_package_parses_and_is_optional() {
1428 let cli = Cli::try_parse_from(["xbp", "install"]).expect("parse");
1429 let Some(Commands::Install {
1430 list,
1431 force,
1432 package,
1433 }) = cli.command
1434 else {
1435 panic!("expected install command");
1436 };
1437 assert!(!list);
1438 assert!(!force);
1439 assert!(package.is_none());
1440 }
1441
1442 #[test]
1443 fn install_with_package_still_parses() {
1444 let cli = Cli::try_parse_from(["xbp", "install", "docker"]).expect("parse");
1445 let Some(Commands::Install {
1446 list,
1447 force,
1448 package,
1449 }) = cli.command
1450 else {
1451 panic!("expected install command");
1452 };
1453 assert!(!list);
1454 assert!(!force);
1455 assert_eq!(package.as_deref(), Some("docker"));
1456 }
1457
1458 #[test]
1459 fn install_force_flag_parses() {
1460 let cli = Cli::try_parse_from(["xbp", "install", "--force", "nx-brew"]).expect("parse");
1461 let Some(Commands::Install {
1462 list,
1463 force,
1464 package,
1465 }) = cli.command
1466 else {
1467 panic!("expected install command");
1468 };
1469 assert!(!list);
1470 assert!(force);
1471 assert_eq!(package.as_deref(), Some("nx-brew"));
1472 }
1473
1474 #[test]
1475 fn install_list_flag_parses() {
1476 let cli = Cli::try_parse_from(["xbp", "install", "--list"]).expect("parse");
1477 let Some(Commands::Install {
1478 list,
1479 force: _,
1480 package,
1481 }) = cli.command
1482 else {
1483 panic!("expected install command");
1484 };
1485 assert!(list);
1486 assert!(package.is_none());
1487 }
1488
1489 #[test]
1490 fn install_short_list_flag_parses() {
1491 let cli = Cli::try_parse_from(["xbp", "install", "-l"]).expect("parse");
1492 let Some(Commands::Install {
1493 list,
1494 force: _,
1495 package,
1496 }) = cli.command
1497 else {
1498 panic!("expected install command");
1499 };
1500 assert!(list);
1501 assert!(package.is_none());
1502 }
1503
1504 #[test]
1505 fn install_ls_alias_parses_as_package() {
1506 let cli = Cli::try_parse_from(["xbp", "install", "ls"]).expect("parse");
1507 let Some(Commands::Install {
1508 list,
1509 force: _,
1510 package,
1511 }) = cli.command
1512 else {
1513 panic!("expected install command");
1514 };
1515 assert!(!list);
1516 assert_eq!(package.as_deref(), Some("ls"));
1517 }
1518
1519 #[test]
1520 fn version_release_make_latest_parses_explicit_value() {
1521 let cli = Cli::try_parse_from([
1522 "xbp",
1523 "version",
1524 "release",
1525 "--version",
1526 "1.2.3",
1527 "--make-latest",
1528 "false",
1529 ])
1530 .expect("parse");
1531 let Some(Commands::Version(version_cmd)) = cli.command else {
1532 panic!("expected version command");
1533 };
1534 assert!(version_cmd.explicit_version.is_none());
1535 let Some(VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
1536 panic!("expected version release subcommand");
1537 };
1538 assert!(matches!(
1539 release_cmd.make_latest,
1540 VersionReleaseLatest::False
1541 ));
1542 }
1543
1544 #[test]
1545 fn version_release_make_latest_defaults_to_legacy() {
1546 let cli = Cli::try_parse_from(["xbp", "version", "release", "--version", "1.2.3"])
1547 .expect("parse");
1548 let Some(Commands::Version(version_cmd)) = cli.command else {
1549 panic!("expected version command");
1550 };
1551 let Some(VersionSubCommand::Release(release_cmd)) = version_cmd.command else {
1552 panic!("expected version release subcommand");
1553 };
1554 assert!(matches!(
1555 release_cmd.make_latest,
1556 VersionReleaseLatest::Legacy
1557 ));
1558 }
1559
1560 #[test]
1561 fn version_explicit_flag_parses() {
1562 let cli = Cli::try_parse_from(["xbp", "version", "--version", "1.2.3"]).expect("parse");
1563 let Some(Commands::Version(version_cmd)) = cli.command else {
1564 panic!("expected version command");
1565 };
1566 assert_eq!(version_cmd.explicit_version.as_deref(), Some("1.2.3"));
1567 assert!(version_cmd.target.is_none());
1568 assert!(version_cmd.command.is_none());
1569 }
1570
1571 #[test]
1572 fn version_short_explicit_flag_parses_with_alias() {
1573 let cli = Cli::try_parse_from(["xbp", "v", "-v", "1.2.3"]).expect("parse");
1574 let Some(Commands::Version(version_cmd)) = cli.command else {
1575 panic!("expected version command");
1576 };
1577 assert_eq!(version_cmd.explicit_version.as_deref(), Some("1.2.3"));
1578 assert!(version_cmd.target.is_none());
1579 assert!(version_cmd.command.is_none());
1580 }
1581}