use std::process::ExitCode;
use clap::Parser;
use nbspec::cli::{Cli, Command, ServeService, failure_report};
use nbspec::mcp::{self, McpConfiguration};
use nbspec::operations::{self, OperationError};
#[tokio::main]
async fn main() -> ExitCode {
let arguments = Cli::parse();
match dispatch(arguments).await {
Ok(()) => ExitCode::SUCCESS,
Err(DispatchError::ChangeVerb(error)) => {
eprintln!("{}", failure_report(&error));
ExitCode::FAILURE
}
Err(DispatchError::Service(error)) => {
eprintln!("Error: {error:#}");
ExitCode::FAILURE
}
}
}
enum DispatchError {
ChangeVerb(OperationError),
Service(anyhow::Error),
}
async fn dispatch(arguments: Cli) -> Result<(), DispatchError> {
match &arguments.command {
Command::Serve { service } => run_service(service, arguments.notebook.as_deref())
.await
.map_err(DispatchError::Service),
verb => run_change_verb(&arguments, verb).await,
}
}
async fn run_service(service: &ServeService, notebook: Option<&str>) -> anyhow::Result<()> {
match service {
ServeService::Mcp => {
let configuration = McpConfiguration {
notebook: notebook.map(String::from),
};
mcp::run(configuration).await
}
}
}
fn resolve_comment(
comment: Option<&str>,
comment_file: Option<&str>,
) -> Result<Option<String>, DispatchError> {
let Some(path) = comment_file else {
return Ok(comment.map(String::from));
};
let content = if path == "-" {
let mut buffer = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut buffer)
.map(|_| buffer)
.map_err(|error| {
anyhow::Error::new(error)
.context("cannot read the review comment from standard input")
})
} else {
std::fs::read_to_string(path).map_err(|error| {
anyhow::Error::new(error).context(format!("cannot read the review comment file {path}"))
})
}
.map_err(DispatchError::Service)?;
Ok(Some(content))
}
async fn run_change_verb(arguments: &Cli, command: &Command) -> Result<(), DispatchError> {
let config = nb_api::Config {
notebook: arguments.notebook.clone(),
..nb_api::Config::default()
};
let client = nb_api::NbClient::new(&config)
.map_err(|e| DispatchError::Service(anyhow::Error::msg(e.to_string())))?;
let notebook = arguments.notebook.as_deref();
let output = match command {
Command::Create { change_id, title } => {
operations::create(&client, notebook, change_id, title.as_deref()).await
}
Command::Display { change_id, full } => {
operations::display(&client, notebook, change_id, *full).await
}
Command::Render { change_id, diff } => {
operations::render(&client, notebook, change_id, *diff).await
}
Command::Merge { change_id, force } => {
operations::merge(&client, notebook, change_id, *force).await
}
Command::Validate { change_id } => operations::validate(&client, notebook, change_id).await,
Command::Review {
change_id,
gate,
verdict,
comment,
comment_file,
reviewer,
} => {
let comment = resolve_comment(comment.as_deref(), comment_file.as_deref())?;
operations::review(
&client,
notebook,
change_id,
gate,
(*verdict).into(),
reviewer.as_deref(),
comment.as_deref(),
)
.await
}
Command::Serve { .. } => unreachable!("serve dispatched in dispatch()"),
};
match output {
Ok(outcome) => {
println!("{}", outcome.text);
Ok(())
}
Err(error) => Err(DispatchError::ChangeVerb(error)),
}
}