1use std::process;
2
3use clap::{CommandFactory, FromArgMatches};
4
5use crate::cli::args::{Cli, Commands, FindFilters, IndexFlags};
6use crate::cli::help::{filter_examples, filter_long_help};
7use crate::commands::init as init_commands;
8use crate::dispatch::{CommandContext, dispatch};
9use crate::error::AppError;
10use crate::hints::{CommonHintFlags, HintContext, HintSource};
11use crate::output::{CommandOutcome, Format};
12use crate::output_pipeline::{COUNT_UNSUPPORTED_ERROR, OutputPipeline};
13use hyalo_core::index::SnapshotIndex;
14
15fn effective_index_path_for(
22 cmd: &Commands,
23 vault_dir: &std::path::Path,
24) -> Option<std::path::PathBuf> {
25 use crate::cli::args::{LinksAction, PropertiesAction, TagsAction, TaskAction};
26
27 let flags: Option<&IndexFlags> = match cmd {
28 Commands::Find { index_flags, .. }
29 | Commands::Summary { index_flags, .. }
30 | Commands::Backlinks { index_flags, .. }
31 | Commands::Set { index_flags, .. }
32 | Commands::Remove { index_flags, .. }
33 | Commands::Append { index_flags, .. }
34 | Commands::Mv { index_flags, .. }
35 | Commands::Read { index_flags, .. }
36 | Commands::Lint { index_flags, .. } => Some(index_flags),
37 Commands::Tags { action } => match action {
38 Some(
39 TagsAction::Summary { index_flags, .. } | TagsAction::Rename { index_flags, .. },
40 ) => Some(index_flags),
41 None => None,
42 },
43 Commands::Properties { action } => match action {
44 Some(
45 PropertiesAction::Summary { index_flags, .. }
46 | PropertiesAction::Rename { index_flags, .. },
47 ) => Some(index_flags),
48 None => None,
49 },
50 Commands::Links { action } => match action {
51 LinksAction::Fix { index_flags, .. } | LinksAction::Auto { index_flags, .. } => {
52 Some(index_flags)
53 }
54 },
55 Commands::Task { action } => match action {
56 TaskAction::Read { index_flags, .. }
57 | TaskAction::Toggle { index_flags, .. }
58 | TaskAction::Set { index_flags, .. } => Some(index_flags),
59 },
60 Commands::CreateIndex { .. }
61 | Commands::DropIndex { .. }
62 | Commands::Init { .. }
63 | Commands::Deinit
64 | Commands::Completion { .. }
65 | Commands::Views { .. }
66 | Commands::Types { .. } => None,
67 };
68
69 let raw = flags?.effective_index_path(vault_dir)?;
70 let resolved = if raw.is_relative() && flags.and_then(|f| f.index_file.as_ref()).is_some() {
75 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
76 cwd.join(&raw)
77 } else {
78 raw
79 };
80 Some(resolved)
81}
82
83fn task_selector(line: &[usize], section: Option<&String>, all: bool) -> Option<String> {
85 if all {
86 Some("all".to_owned())
87 } else if let Some(s) = section {
88 Some(format!("section:{s}"))
89 } else if line.len() > 1 {
90 Some("lines".to_owned())
91 } else {
92 None
93 }
94}
95
96#[allow(clippy::too_many_lines)]
97pub fn run() {
98 match run_inner() {
99 Ok(()) => {
100 crate::warn::flush_summary();
101 }
102 Err(e) => {
103 crate::warn::flush_summary();
104 let code = match e {
105 AppError::User(msg) => {
106 if !msg.is_empty() {
107 eprintln!("{msg}");
108 }
109 1
110 }
111 AppError::Internal(err) => {
112 let s = err.to_string();
113 if !s.is_empty() {
114 eprintln!("Error: {err}");
115 }
116 2
117 }
118 AppError::Clap(err) => {
119 let code = err.exit_code();
120 let _ = err.print();
121 code
122 }
123 AppError::Exit(code) => code,
124 };
125 process::exit(code);
126 }
127 }
128}
129
130#[allow(clippy::too_many_lines)]
131fn run_inner() -> Result<(), AppError> {
132 let early_quiet = std::env::args().any(|a| a == "--quiet" || a == "-q");
134 crate::warn::init(early_quiet);
135
136 let config = crate::config::load_config();
140
141 let hide_dir = config
146 .dir
147 .components()
148 .ne(std::path::Path::new(".").components());
149 let hide_format = config.format != "json";
150
151 let mut cmd = Cli::command();
152 if hide_dir {
153 cmd = cmd.mut_arg("dir", |a| a.hide(true));
154 }
155 if hide_format {
156 cmd = cmd.mut_arg("format", |a| a.hide(true));
157 }
158
159 cmd = cmd
163 .after_help(filter_examples(hide_dir, hide_format))
164 .after_long_help(filter_long_help(hide_dir, hide_format));
165
166 let raw_args: Vec<String> = std::env::args().collect();
172 let matches = match cmd.try_get_matches_from(raw_args.iter().map(String::as_str)) {
173 Ok(m) => m,
174 Err(e) => {
175 if e.kind() == clap::error::ErrorKind::UnknownArgument
179 && crate::suggest::unknown_arg_is(&e, "--filter")
180 {
181 eprintln!(
182 "error: unexpected argument '--filter' found\n\n\
183 tip: did you mean '--property'?\n\n\
184 Example: hyalo find --property status=planned\n"
185 );
186 return Err(AppError::Exit(2));
187 }
188
189 if e.kind() == clap::error::ErrorKind::UnknownArgument
199 && crate::suggest::top_level_subcommand(&raw_args, &Cli::command())
200 == Some("append")
201 && (crate::suggest::unknown_arg_is(&e, "--tag")
202 || crate::suggest::unknown_arg_is(&e, "-t"))
203 {
204 eprintln!(
205 "error: `hyalo append` does not accept --tag (tags are scalar list items, not appendable)\n\n\
206 hint: use `hyalo set <file> --tag <tag>` to add a tag\n"
207 );
208 return Err(AppError::Exit(2));
209 }
210
211 if matches!(
214 e.kind(),
215 clap::error::ErrorKind::InvalidSubcommand | clap::error::ErrorKind::UnknownArgument
216 ) && let Some(suggestion) =
217 crate::suggest::suggest_subcommand_correction(&raw_args, &Cli::command())
218 {
219 eprintln!("{e}\n tip: did you mean:\n\n {suggestion}\n");
220 return Err(AppError::Exit(2));
221 }
222
223 if e.kind() == clap::error::ErrorKind::InvalidSubcommand {
228 use clap::error::{ContextKind, ContextValue};
229 let parent_is_properties = raw_args
230 .iter()
231 .any(|a| a == "properties" || a == "property");
232 if let Some(invalid) = e.context().find_map(|(k, v)| {
233 if k == ContextKind::InvalidSubcommand {
234 if let ContextValue::String(s) = v {
235 Some(s.as_str())
236 } else {
237 None
238 }
239 } else {
240 None
241 }
242 }) {
243 if parent_is_properties {
245 eprintln!(
246 "{e}\n hint: 'properties' has subcommands; try 'hyalo properties summary' or 'hyalo properties rename'\n"
247 );
248 return Err(AppError::Exit(2));
249 }
250 for (target, suggestion) in [("version", "--version"), ("help", "--help")] {
251 if strsim::damerau_levenshtein(invalid, target) <= 2 {
252 eprintln!("{e}\n tip: did you mean `hyalo {suggestion}`?\n");
253 return Err(AppError::Exit(2));
254 }
255 }
256 }
257 }
258
259 return Err(AppError::Clap(e));
260 }
261 };
262 let mut cli = match Cli::from_arg_matches(&matches) {
263 Ok(c) => c,
264 Err(e) => return Err(AppError::Clap(e)),
265 };
266
267 crate::warn::init(cli.quiet);
270
271 if cli.count
276 && matches!(
277 cli.command,
278 Commands::Init { .. } | Commands::Deinit | Commands::Completion { .. }
279 )
280 {
281 eprintln!("{COUNT_UNSUPPORTED_ERROR}");
282 return Err(AppError::Exit(2));
283 }
284 if let Commands::Init { claude } = cli.command {
285 let init_dir = cli.dir.as_deref().and_then(|p| p.to_str());
286 match init_commands::run_init(init_dir, claude) {
287 Ok(CommandOutcome::Success { output, .. } | CommandOutcome::RawOutput(output)) => {
288 println!("{output}");
289 return Ok(());
290 }
291 Ok(CommandOutcome::UserError(output)) => return Err(AppError::User(output)),
292 Err(e) => return Err(AppError::Internal(e)),
293 }
294 }
295 if let Commands::Deinit = cli.command {
296 match init_commands::run_deinit() {
297 Ok(CommandOutcome::Success { output, .. } | CommandOutcome::RawOutput(output)) => {
298 println!("{output}");
299 return Ok(());
300 }
301 Ok(CommandOutcome::UserError(output)) => return Err(AppError::User(output)),
302 Err(e) => return Err(AppError::Internal(e)),
303 }
304 }
305 if let Commands::Completion { shell } = cli.command {
306 let mut cmd = Cli::command();
307 clap_complete::generate(shell, &mut cmd, "hyalo", &mut std::io::stdout());
308 return Ok(());
309 }
310 let dir_from_cli = cli.dir.is_some();
314 let format_from_cli = cli.format.is_some();
315 let hints_from_cli = cli.hints;
316 let (dir, config) = if let Some(cli_dir) = cli.dir {
324 if !cli_dir.exists() {
326 return Err(AppError::User(format!(
327 "Error: --dir path '{}' does not exist.",
328 cli_dir.display()
329 )));
330 }
331 if cli_dir.is_file() {
332 return Err(AppError::User(format!(
333 "Error: --dir path '{}' is a file, not a directory. Use --file to target a single file.",
334 cli_dir.display()
335 )));
336 }
337 let target_config = crate::config::load_config_from(&cli_dir);
338 (cli_dir, target_config)
339 } else {
340 let vault_dir = config.dir.clone();
341 (vault_dir, config)
342 };
343 let config_dir = config.config_dir.clone();
345
346 if !dir.exists() {
349 return Err(AppError::User(format!(
350 "Error: --dir path '{}' does not exist.",
351 dir.display()
352 )));
353 }
354 if dir.is_file() {
355 return Err(AppError::User(format!(
356 "Error: --dir path '{}' is a file, not a directory. Use --file to target a single file.",
357 dir.display()
358 )));
359 }
360
361 let site_prefix_owned: Option<String> = if cli.site_prefix.is_some() {
371 cli.site_prefix.filter(|s| !s.is_empty())
373 } else if config.site_prefix.is_some() {
374 config.site_prefix.filter(|s| !s.is_empty())
376 } else {
377 match std::fs::canonicalize(&dir) {
379 Ok(canonical) => canonical
380 .file_name()
381 .and_then(|n| n.to_str())
382 .map(std::borrow::ToOwned::to_owned),
383 Err(_) => {
384 dir.file_name()
388 .and_then(|n| n.to_str())
389 .filter(|s| *s != ".")
390 .map(std::borrow::ToOwned::to_owned)
391 }
392 }
393 };
394 let site_prefix = site_prefix_owned.as_deref();
395 let format = if let Some(f) = cli.format {
397 f
398 } else if let Some(fmt) = Format::from_str_opt(&config.format) {
399 fmt
400 } else {
401 eprintln!(
402 "Invalid output format '{}' in .hyalo.toml; supported formats are: json, text",
403 config.format
404 );
405 return Err(AppError::Exit(2));
406 };
407 let hints_flag = if cli.hints {
408 true
409 } else if cli.no_hints {
410 false
411 } else {
412 config.hints
413 };
414
415 if let Commands::Find {
417 view: Some(ref view_name),
418 ref mut filters,
419 ..
420 } = cli.command
421 {
422 let views = crate::commands::views::load_views(&config_dir);
423 match views.get(view_name) {
424 Some(base) => {
425 let overlay = std::mem::take(filters);
426 *filters = base.clone();
427 filters.merge_from(&overlay);
428 }
429 None => {
430 return Err(AppError::User(format!(
431 "Error: unknown view '{view_name}'\n\n tip: run 'hyalo views list' to see available views"
432 )));
433 }
434 }
435 }
436
437 if let Commands::Find {
441 ref mut pattern,
442 ref filters,
443 ..
444 } = cli.command
445 && pattern.is_none()
446 && filters.regexp.is_none()
447 && let Some(ref view_pattern) = filters.pattern
448 {
449 *pattern = Some(view_pattern.clone());
450 }
451
452 let jq_filter = cli.jq.as_deref();
454
455 let format = if !format_from_cli
458 && jq_filter.is_none()
459 && matches!(cli.command, Commands::Read { .. })
460 {
461 Format::Text
462 } else {
463 format
464 };
465 if cli.count && jq_filter.is_some() {
467 eprintln!("Error: --count cannot be combined with --jq");
468 eprintln!(
469 " --count prints the bare total; --jq applies a custom filter — use one or the other"
470 );
471 return Err(AppError::Exit(2));
472 }
473 if jq_filter.is_some() && format != Format::Json {
474 eprintln!("Error: --jq cannot be combined with --format {format}");
475 eprintln!(" --jq always operates on JSON output; drop --format or use --format json");
476 return Err(AppError::Exit(2));
477 }
478 let effective_format = Format::Json;
481
482 let hint_ctx = if hints_flag && jq_filter.is_none() {
486 let common = CommonHintFlags {
490 dir: if dir_from_cli {
491 dir.to_str()
492 .map(std::borrow::ToOwned::to_owned)
493 .filter(|s| s != ".")
494 } else {
495 None
496 },
497 format: if format_from_cli {
498 Some(format.to_string())
499 } else {
500 None
501 },
502 hints: hints_from_cli,
503 };
504
505 match &cli.command {
506 Commands::Summary { glob, .. } => {
507 let mut ctx = HintContext::from_common(HintSource::Summary, &common);
508 ctx.glob.clone_from(glob);
509 Some(ctx)
510 }
511 Commands::Properties {
512 action: Some(crate::cli::args::PropertiesAction::Summary { glob, limit, .. }),
513 } => {
514 let mut ctx = HintContext::from_common(HintSource::PropertiesSummary, &common);
515 ctx.glob.clone_from(glob);
516 ctx.has_limit = limit.is_some();
517 Some(ctx)
518 }
519 Commands::Tags {
520 action: Some(crate::cli::args::TagsAction::Summary { glob, limit, .. }),
521 } => {
522 let mut ctx = HintContext::from_common(HintSource::TagsSummary, &common);
523 ctx.glob.clone_from(glob);
524 ctx.has_limit = limit.is_some();
525 Some(ctx)
526 }
527 Commands::Tags { action: None } => {
528 Some(HintContext::from_common(HintSource::TagsSummary, &common))
530 }
531 Commands::Find {
532 pattern,
533 file_positional,
534 view,
535 filters:
536 FindFilters {
537 glob,
538 regexp,
539 properties,
540 tag,
541 task,
542 file,
543 fields,
544 sort,
545 limit,
546 sections,
547 ..
548 },
549 ..
550 } => {
551 let file = if file_positional.is_empty() {
553 file
554 } else {
555 file_positional
556 };
557 let mut ctx = HintContext::from_common(HintSource::Find, &common);
558 ctx.glob.clone_from(glob);
559 ctx.fields.clone_from(fields);
560 ctx.sort.clone_from(sort);
561 ctx.has_limit = limit.is_some();
562 ctx.has_body_search = pattern.is_some();
563 ctx.body_pattern.clone_from(pattern);
564 ctx.has_regex_search = regexp.is_some();
565 ctx.property_filters.clone_from(properties);
566 ctx.tag_filters.clone_from(tag);
567 ctx.task_filter.clone_from(task);
568 ctx.file_targets.clone_from(file);
569 ctx.section_filters.clone_from(sections);
570 ctx.view_name.clone_from(view);
571 Some(ctx)
572 }
573 Commands::Set {
574 file_positional,
575 file,
576 glob,
577 dry_run,
578 ..
579 } => {
580 let mut ctx = HintContext::from_common(HintSource::Set, &common);
581 ctx.glob.clone_from(glob);
582 let src = if file_positional.is_empty() {
583 file
584 } else {
585 file_positional
586 };
587 ctx.file_targets.clone_from(src);
588 ctx.dry_run = *dry_run;
589 Some(ctx)
590 }
591 Commands::Remove {
592 file_positional,
593 file,
594 glob,
595 dry_run,
596 ..
597 } => {
598 let mut ctx = HintContext::from_common(HintSource::Remove, &common);
599 ctx.glob.clone_from(glob);
600 let src = if file_positional.is_empty() {
601 file
602 } else {
603 file_positional
604 };
605 ctx.file_targets.clone_from(src);
606 ctx.dry_run = *dry_run;
607 Some(ctx)
608 }
609 Commands::Append {
610 file_positional,
611 file,
612 glob,
613 dry_run,
614 ..
615 } => {
616 let mut ctx = HintContext::from_common(HintSource::Append, &common);
617 ctx.glob.clone_from(glob);
618 let src = if file_positional.is_empty() {
619 file
620 } else {
621 file_positional
622 };
623 ctx.file_targets.clone_from(src);
624 ctx.dry_run = *dry_run;
625 Some(ctx)
626 }
627 Commands::Read {
628 file_positional,
629 file,
630 ..
631 } => {
632 let mut ctx = HintContext::from_common(HintSource::Read, &common);
633 if let Some(f) = file_positional.as_ref().or(file.as_ref()) {
634 ctx.file_targets = vec![f.clone()];
635 }
636 Some(ctx)
637 }
638 Commands::Backlinks {
639 file_positional,
640 file,
641 limit,
642 ..
643 } => {
644 let mut ctx = HintContext::from_common(HintSource::Backlinks, &common);
645 if let Some(f) = file_positional.as_ref().or(file.as_ref()) {
646 ctx.file_targets = vec![f.clone()];
647 }
648 ctx.has_limit = limit.is_some();
649 Some(ctx)
650 }
651 Commands::Mv {
652 file_positional,
653 file,
654 dry_run,
655 ..
656 } => {
657 let mut ctx = HintContext::from_common(HintSource::Mv, &common);
658 if let Some(f) = file_positional.as_ref().or(file.as_ref()) {
659 ctx.file_targets = vec![f.clone()];
660 }
661 ctx.dry_run = *dry_run;
662 Some(ctx)
663 }
664 Commands::Task { action } => {
665 let (source, file_pos, file_flag, selector) = match action {
666 crate::cli::args::TaskAction::Toggle {
667 file_positional,
668 file,
669 line,
670 section,
671 all,
672 dry_run: _,
673 ..
674 } => (
675 HintSource::TaskToggle,
676 file_positional,
677 file,
678 task_selector(line, section.as_ref(), *all),
679 ),
680 crate::cli::args::TaskAction::Set {
681 file_positional,
682 file,
683 line,
684 section,
685 all,
686 ..
687 } => (
688 HintSource::TaskSetStatus,
689 file_positional,
690 file,
691 task_selector(line, section.as_ref(), *all),
692 ),
693 crate::cli::args::TaskAction::Read {
694 file_positional,
695 file,
696 line,
697 section,
698 all,
699 ..
700 } => (
701 HintSource::TaskRead,
702 file_positional,
703 file,
704 task_selector(line, section.as_ref(), *all),
705 ),
706 };
707 let mut ctx = HintContext::from_common(source, &common);
708 if let Some(f) = file_pos.as_ref().or(file_flag.as_ref()) {
709 ctx.file_targets = vec![f.clone()];
710 }
711 ctx.task_selector = selector;
712 Some(ctx)
713 }
714 Commands::Links { action } => match action {
715 crate::cli::args::LinksAction::Fix { apply, glob, .. } => {
716 let mut ctx = HintContext::from_common(HintSource::LinksFix, &common);
717 ctx.glob.clone_from(glob);
718 ctx.dry_run = !apply;
719 Some(ctx)
720 }
721 crate::cli::args::LinksAction::Auto {
722 apply,
723 glob,
724 file,
725 min_length,
726 exclude_title,
727 ..
728 } => {
729 let mut ctx = HintContext::from_common(HintSource::LinksAuto, &common);
730 ctx.glob.clone_from(glob);
731 ctx.dry_run = !apply;
732 ctx.auto_link_file.clone_from(file);
733 ctx.auto_link_min_length = Some(*min_length);
734 ctx.auto_link_exclude_titles.clone_from(exclude_title);
735 Some(ctx)
736 }
737 },
738 Commands::CreateIndex { output, .. } => {
739 let mut ctx = HintContext::from_common(HintSource::CreateIndex, &common);
740 ctx.index_path = output.as_ref().map(|p| p.to_string_lossy().into_owned());
741 Some(ctx)
742 }
743 Commands::DropIndex { .. } => {
744 Some(HintContext::from_common(HintSource::DropIndex, &common))
745 }
746 Commands::Lint {
747 file_positional,
748 file,
749 glob,
750 r#type: _,
751 fix: _,
752 dry_run,
753 limit,
754 ..
755 } => {
756 let mut ctx = HintContext::from_common(HintSource::Lint, &common);
757 ctx.glob.clone_from(glob);
758 ctx.dry_run = *dry_run;
759 ctx.has_limit = limit.is_some();
760 let mut targets: Vec<String> = file.clone();
761 if let Some(pos) = file_positional {
762 targets.insert(0, pos.clone());
763 }
764 ctx.file_targets = targets;
765 Some(ctx)
766 }
767 Commands::Types { action } => {
768 use crate::cli::args::TypesAction;
769 let subcommand = match action {
770 Some(TypesAction::List) | None => Some("list".to_owned()),
771 Some(TypesAction::Show { .. }) => Some("show".to_owned()),
772 Some(TypesAction::Remove { .. }) => Some("remove".to_owned()),
773 Some(TypesAction::Set { .. }) => Some("set".to_owned()),
774 };
775 Some(HintContext::from_common(
776 HintSource::Types { subcommand },
777 &common,
778 ))
779 }
780 Commands::Properties { .. }
781 | Commands::Tags { .. }
782 | Commands::Init { .. }
783 | Commands::Deinit
784 | Commands::Completion { .. }
785 | Commands::Views { .. } => None,
786 }
787 } else {
788 None
789 };
790
791 let index_path_buf: Option<std::path::PathBuf> = effective_index_path_for(&cli.command, &dir);
795
796 let mut snapshot_index: Option<SnapshotIndex> = if let Some(ref p) = index_path_buf {
797 match SnapshotIndex::load(p) {
798 Ok(Some(idx)) => {
799 let canonical_dir = std::fs::canonicalize(&dir).unwrap_or_else(|_| dir.clone());
802 let vault_dir_str = canonical_dir.to_string_lossy();
803 if idx.validate(&vault_dir_str, site_prefix) {
804 Some(idx)
805 } else {
806 let (hdr_vault, hdr_prefix, _, _) = idx.header_info();
807 crate::warn::warn(format!(
808 "index was built for vault '{hdr_vault}' (prefix {hdr_prefix:?}) but current \
809 vault is '{vault_dir_str}' (prefix {site_prefix:?}); falling back to disk scan",
810 ));
811 None
812 }
813 }
814 Ok(None) => None, Err(e) => {
816 crate::warn::warn(format!(
817 "failed to load index: {e}; falling back to disk scan"
818 ));
819 None
820 }
821 }
822 } else {
823 None
824 };
825
826 let config_language_owned = config.search_language.clone();
827 let config_default_limit = config.default_limit;
828 let schema = config.schema;
829 let frontmatter_link_props_owned = config.frontmatter_link_props;
830 let validate_on_write = config.validate_on_write;
831 let lint_ignore = config.lint_ignore;
832 let case_insensitive_mode = config.case_insensitive_mode;
833
834 if let Some(idx) = snapshot_index.as_mut() {
838 idx.set_frontmatter_link_props(frontmatter_link_props_owned.clone());
839 }
840 let mut ctx = CommandContext {
841 dir: &dir,
842 config_dir: &config_dir,
843 site_prefix,
844 effective_format,
845 user_format: format,
846 snapshot_index: &mut snapshot_index,
847 index_path: index_path_buf.as_deref(),
848 config_language: config_language_owned.as_deref(),
849 frontmatter_link_props: frontmatter_link_props_owned.as_deref(),
850 schema: &schema,
851 validate_on_write,
852 lint_ignore: &lint_ignore,
853 case_insensitive_mode,
854 exit_code_override: None,
855 config_default_limit,
856 programmatic_output: jq_filter.is_some() || cli.count,
857 };
858 let result = dispatch(cli.command, &mut ctx);
859 let exit_code_override = ctx.exit_code_override;
860
861 let pipeline = OutputPipeline {
862 user_format: format,
863 jq_filter,
864 hint_ctx: hint_ctx.as_ref(),
865 count: cli.count,
866 };
867 let code = pipeline.finalize(result);
868 let final_code = exit_code_override.unwrap_or(code);
870 if final_code == 0 {
871 Ok(())
872 } else {
873 Err(AppError::Exit(final_code))
874 }
875}