Skip to main content

moonlight_cli/
lib.rs

1mod args;
2mod batch;
3mod command;
4mod command_form;
5mod config;
6mod eval;
7mod eval_config;
8mod execute;
9mod input;
10mod run_once;
11mod types;
12mod worktree;
13
14use anyhow::Context;
15use args::{Cli, CliCommand, EvalCommand};
16use clap::Parser;
17use config::CliDefaults;
18use moonlight_core::{
19    config::load_optional_config,
20    report::{render_report, ReportFormat},
21    review::{ReviewStatus, ReviewStore, ReviewUpdate},
22    storage::JsonlStorageReader,
23    Adapter, Classification, RunFilter,
24};
25use std::process::ExitCode;
26
27#[derive(Debug)]
28pub struct ExitCodeError {
29    code: u8,
30    message: String,
31}
32
33impl ExitCodeError {
34    fn new(code: u8, error: anyhow::Error) -> Self {
35        Self {
36            code,
37            message: format!("{error:#}"),
38        }
39    }
40
41    pub fn code(&self) -> u8 {
42        self.code
43    }
44}
45
46impl std::fmt::Display for ExitCodeError {
47    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        self.message.fmt(formatter)
49    }
50}
51
52impl std::error::Error for ExitCodeError {}
53
54pub async fn run_cli() -> anyhow::Result<ExitCode> {
55    let cli = Cli::parse();
56    let file_config = load_optional_config(cli.config.config.as_deref(), cli.config.no_config)?;
57    let defaults = CliDefaults::from_config(&file_config);
58
59    match cli.command {
60        CliCommand::Run(args) => {
61            run_once::run(args, &defaults).await?;
62            Ok(ExitCode::SUCCESS)
63        }
64        CliCommand::Batch(args) => {
65            batch::batch(args, &defaults).await?;
66            Ok(ExitCode::SUCCESS)
67        }
68        CliCommand::List(args) => {
69            let storage = JsonlStorageReader::new(storage_path(args.output.storage, &defaults));
70            let page = storage
71                .filtered_page(
72                    &run_filter(args.classification, args.adapter, args.query, args.status)?,
73                    args.limit.unwrap_or(100),
74                    args.offset,
75                )
76                .await?;
77            print_json(&page, args.output.compact)?;
78            Ok(ExitCode::SUCCESS)
79        }
80        CliCommand::Stats(args) => {
81            let storage = JsonlStorageReader::new(storage_path(args.storage, &defaults));
82            print_json(&storage.stats().await?, args.compact)?;
83            Ok(ExitCode::SUCCESS)
84        }
85        CliCommand::Show(args) => {
86            let storage = JsonlStorageReader::new(storage_path(args.output.storage, &defaults));
87            let run = storage
88                .get(args.id)
89                .await?
90                .with_context(|| format!("comparison run {} was not found", args.id))?;
91            print_json(&run, args.output.compact)?;
92            Ok(ExitCode::SUCCESS)
93        }
94        CliCommand::Report(args) => {
95            let storage = JsonlStorageReader::new(storage_path(args.storage, &defaults));
96            let run = storage
97                .get(args.id)
98                .await?
99                .with_context(|| format!("comparison run {} was not found", args.id))?;
100            let format = args.format.parse::<ReportFormat>()?;
101            println!("{}", render_report(&run, None, format)?);
102            Ok(ExitCode::SUCCESS)
103        }
104        CliCommand::Review(args) => {
105            let store = ReviewStore::load(defaults.review_state_path.clone()).await?;
106            let state = store
107                .put(
108                    args.id,
109                    ReviewUpdate {
110                        status: args.status.parse::<ReviewStatus>()?,
111                        note: args.note,
112                        tags: Some(args.tags),
113                    },
114                )
115                .await?;
116            print_json(&state, false)?;
117            Ok(ExitCode::SUCCESS)
118        }
119        CliCommand::Reviews(args) => {
120            let store = ReviewStore::load(defaults.review_state_path.clone()).await?;
121            let status = args
122                .status
123                .map(|status| status.parse::<ReviewStatus>())
124                .transpose()?;
125            print_json(&store.list(status).await, args.compact)?;
126            Ok(ExitCode::SUCCESS)
127        }
128        CliCommand::Eval(command) => match command {
129            EvalCommand::Run(args) => eval::run(args, &defaults)
130                .await
131                .map_err(|error| ExitCodeError::new(2, error).into()),
132            EvalCommand::Report(args) => eval::report(args, &defaults)
133                .await
134                .map_err(|error| ExitCodeError::new(2, error).into()),
135        },
136    }
137}
138
139fn storage_path(args: args::StorageArgs, defaults: &CliDefaults) -> std::path::PathBuf {
140    args.storage_path
141        .unwrap_or_else(|| defaults.storage_path.clone())
142}
143
144fn print_json(value: &impl serde::Serialize, compact: bool) -> anyhow::Result<()> {
145    if compact {
146        println!("{}", serde_json::to_string(value)?);
147    } else {
148        println!("{}", serde_json::to_string_pretty(value)?);
149    }
150    Ok(())
151}
152
153fn run_filter(
154    classification: Option<String>,
155    adapter: Option<String>,
156    query: Option<String>,
157    status: Option<u16>,
158) -> anyhow::Result<RunFilter> {
159    Ok(RunFilter {
160        classification: classification
161            .map(|value| value.parse::<Classification>())
162            .transpose()?,
163        adapter: adapter.map(|value| value.parse::<Adapter>()).transpose()?,
164        query,
165        status,
166        has_noise: None,
167        has_diff: None,
168    })
169}