1mod commands;
2mod logging;
3mod output;
4
5use std::path::PathBuf;
6
7use clap::{Parser, Subcommand};
8use sdivi_core::ExitCode;
9
10use commands::boundaries::BoundariesSubcmd;
11
12#[derive(Parser)]
14#[command(name = "sdivi", version, about, long_about = None)]
15struct Cli {
16 #[arg(long, default_value = ".")]
18 repo: PathBuf,
19
20 #[command(subcommand)]
21 command: Option<Commands>,
22}
23
24#[derive(Subcommand)]
25enum Commands {
26 Init,
28 Catalog {
30 #[arg(long, default_value = "text")]
32 format: String,
33 },
34 Snapshot {
36 #[arg(long)]
38 commit: Option<String>,
39 #[arg(long, default_value = "text")]
41 format: String,
42 },
43 Diff {
45 prev: PathBuf,
47 curr: PathBuf,
49 #[arg(long, default_value = "text")]
51 format: String,
52 },
53 Check {
55 #[arg(long)]
57 no_write: bool,
58 #[arg(long, default_value = "text")]
60 format: String,
61 },
62 Trend {
64 #[arg(long)]
66 last: Option<usize>,
67 #[arg(long, default_value = "text")]
69 format: String,
70 },
71 Show {
73 id: Option<String>,
75 #[arg(long, default_value = "text")]
77 format: String,
78 },
79 Boundaries {
81 #[command(subcommand)]
82 subcmd: BoundariesSubcmd,
83 },
84}
85
86pub fn run() {
88 let cli = Cli::parse();
89 logging::init();
90
91 let config = match sdivi_config::load_or_default(&cli.repo) {
92 Ok(c) => c,
93 Err(e) => {
94 eprintln!("sdivi: error: {e:#}");
95 std::process::exit(ExitCode::ConfigError.as_i32());
96 }
97 };
98
99 if let Some(Commands::Check { no_write, format }) = &cli.command {
102 match commands::check::run(&cli.repo, &config, *no_write, format) {
103 Ok(code) => std::process::exit(code.as_i32()),
104 Err(e) => {
105 eprintln!("sdivi: error: {e:#}");
106 std::process::exit(error_exit_code(&e).as_i32());
107 }
108 }
109 }
110
111 let result = match cli.command {
112 Some(Commands::Init) => commands::init::run(&cli.repo),
113 Some(Commands::Catalog { format }) => commands::catalog::run(&cli.repo, &config, &format),
114 Some(Commands::Snapshot { commit, format }) => {
115 commands::snapshot::run(&cli.repo, &config, commit.as_deref(), &format)
116 }
117 Some(Commands::Diff { prev, curr, format }) => commands::diff::run(&prev, &curr, &format),
118 Some(Commands::Check { .. }) => unreachable!("handled above"),
119 Some(Commands::Trend { last, format }) => {
120 commands::trend::run(&cli.repo, &config, last, &format)
121 }
122 Some(Commands::Show { id, format }) => {
123 commands::show::run(&cli.repo, &config, id.as_deref(), &format)
124 }
125 Some(Commands::Boundaries { subcmd }) => {
126 commands::boundaries::run(subcmd, &cli.repo, &config)
127 }
128 None => {
129 eprintln!("sdivi: no subcommand given — try `sdivi --help`");
130 return;
131 }
132 };
133
134 if let Err(e) = result {
135 let code = error_exit_code(&e);
136 eprintln!("sdivi: error: {e:#}");
137 std::process::exit(code.as_i32());
138 }
139}
140
141fn error_exit_code(e: &anyhow::Error) -> ExitCode {
147 if e.downcast_ref::<sdivi_config::ConfigError>().is_some() {
148 return ExitCode::ConfigError;
149 }
150 if let Some(pe) = e.downcast_ref::<sdivi_pipeline::PipelineError>() {
151 if matches!(pe, sdivi_pipeline::PipelineError::NoGrammarsAvailable) {
152 return ExitCode::AnalysisError;
153 }
154 }
155 ExitCode::RuntimeError
156}