Skip to main content

difflore_cli/
lib.rs

1#![cfg_attr(
2    test,
3    allow(
4        clippy::unwrap_used,
5        clippy::expect_used,
6        clippy::panic,
7        clippy::many_single_char_names
8    )
9)]
10
11use clap::FromArgMatches;
12
13pub mod cli;
14pub mod commands;
15mod dispatch;
16pub mod error;
17pub mod hook_cache;
18pub mod hook_forward;
19pub mod hook_runtime;
20pub mod hooks;
21pub mod mcp_install;
22pub mod runtime;
23pub mod style;
24
25use cli::{Cli, Commands, StatusLane};
26
27pub async fn run() {
28    // Detect color support once so paint helpers stay cheap afterwards.
29    let _ = style::detect_color_support();
30
31    let matches = cli::build_cli().get_matches();
32    let cli = match Cli::from_arg_matches(&matches) {
33        Ok(c) => c,
34        Err(e) => {
35            e.exit();
36        }
37    };
38    // Cached startup gate: provider/cloud checks are best-effort
39    // and never block command execution.
40    let _ = difflore_core::startup::ensure_ready(false).await;
41
42    // Retired layout migration guard: new installs create per-project
43    // indexes directly. If an old global `context-index.db` is present,
44    // the guard fails closed and leaves the file untouched. Keep this
45    // best-effort so startup can continue with rebuilt per-project state.
46    if let Err(e) = difflore_core::migration::run_if_needed().await {
47        eprintln!(
48            "[difflore] warning: retired per-project index migration refused old state ({e}). \
49             Run `difflore doctor --report` to inspect."
50        );
51    }
52
53    let command = if let Some(command) = cli.command {
54        command
55    } else {
56        // First-run state machine: bare `difflore` can still run the
57        // wizard/welcome once, then falls through to the compact status
58        // surface instead of opening a separate TUI.
59        match commands::welcome::first_run_path(cli.no_interactive).await {
60            commands::welcome::FirstRunPath::LaunchWizard => {
61                if !commands::welcome::run_wizard().await.should_continue_tui() {
62                    return;
63                }
64            }
65            commands::welcome::FirstRunPath::ShowWelcome => {
66                if !commands::welcome::show_welcome_then_continue()
67                    .await
68                    .should_continue_tui()
69                {
70                    return;
71                }
72            }
73            commands::welcome::FirstRunPath::Skip => {}
74        }
75        Commands::Status {
76            json: false,
77            lane: StatusLane::All,
78        }
79    };
80    Box::pin(dispatch::dispatch(command)).await;
81}