#![deny(unreachable_pub)]
mod catalog;
mod config;
mod error;
mod generation;
#[cfg(test)]
mod levels;
mod progress;
mod registry;
mod relpath;
mod result;
mod retrieval;
mod server;
mod tools;
mod transport;
mod watch;
pub use crate::catalog::{Catalog, CatalogHandle, OnBroken};
pub use crate::config::Config;
pub use crate::error::{
CatalogError, CatalogErrorKind, ConfigError, ConfigErrorKind, FaultKind, FaultRef, Faults,
PreparedToolsError, PreparedToolsErrorKind, RunError, RunErrorKind, WatchError, WatchErrorKind,
};
pub(crate) use crate::retrieval::Retrieval;
pub use crate::server::{PreparedTools, PromptForgeServer};
pub(crate) use crate::transport::{serve_http, serve_stdio};
pub use crate::watch::Watcher;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerArgs {
stdio: bool,
config: PathBuf,
}
impl ServerArgs {
#[must_use]
pub fn parse<I>(arguments: I) -> Option<ServerArgs>
where
I: IntoIterator<Item = std::ffi::OsString>,
{
let mut rest = arguments.into_iter();
if rest.next()?.to_str() != Some("serve") {
return None;
}
let first = rest.next()?;
let (stdio, config) = if first.to_str() == Some("--stdio") {
(true, rest.next()?)
} else {
(false, first)
};
if rest.next().is_some() {
return None;
}
Some(ServerArgs {
stdio,
config: PathBuf::from(config),
})
}
#[must_use]
pub fn stdio(&self) -> bool {
self.stdio
}
#[must_use]
pub fn config(&self) -> &Path {
&self.config
}
}
pub fn run(args: &ServerArgs) -> Result<(), RunError> {
let source = args.config.as_path();
let config = Config::load(source)?;
let catalog = Catalog::resolve(&config, OnBroken::Reject)?;
let retrieval = Retrieval::start(&catalog);
let config = Arc::new(config);
let catalog = Arc::new(CatalogHandle::with_retrieval(catalog, retrieval));
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(RunError::runtime)?;
let tools = Arc::new(runtime.block_on(PreparedTools::load(&config))?);
let stdio = args.stdio;
runtime.block_on(async move {
let _watcher = Watcher::start(source, Arc::clone(&config), Arc::clone(&catalog))?;
if stdio {
serve_stdio(config, catalog, tools, shutdown_signal()).await?;
} else {
serve_http(config, catalog, tools, shutdown_signal()).await?;
}
Ok::<(), RunError>(())
})?;
Ok(())
}
async fn shutdown_signal() {
match tokio::signal::ctrl_c().await {
Ok(()) => tracing::info!("shutdown signal received; draining"),
Err(error) => {
tracing::error!("listen for the shutdown signal: {error}");
std::future::pending::<()>().await;
}
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use super::ServerArgs;
fn parse(arguments: &[&str]) -> Option<ServerArgs> {
ServerArgs::parse(arguments.iter().map(OsString::from))
}
#[test]
fn parses_the_http_shape() {
let args = parse(&["serve", "prompts.toml"]).expect("serve <config> is accepted");
assert!(!args.stdio);
assert_eq!(args.config, std::path::Path::new("prompts.toml"));
}
#[test]
fn parses_the_stdio_shape() {
let args = parse(&["serve", "--stdio", "prompts.toml"]).expect("serve --stdio <config>");
assert!(args.stdio);
assert_eq!(args.config, std::path::Path::new("prompts.toml"));
}
#[test]
fn rejects_a_flag_after_the_config() {
assert!(parse(&["serve", "prompts.toml", "--stdio"]).is_none());
}
#[test]
fn rejects_a_trailing_extra_argument() {
assert!(parse(&["serve", "prompts.toml", "extra"]).is_none());
assert!(parse(&["serve", "--stdio", "prompts.toml", "extra"]).is_none());
}
#[test]
fn rejects_a_missing_config_and_a_missing_subcommand() {
assert!(parse(&[]).is_none());
assert!(parse(&["serve"]).is_none());
assert!(parse(&["serve", "--stdio"]).is_none());
assert!(parse(&["run", "prompts.toml"]).is_none());
assert!(parse(&["--stdio", "prompts.toml"]).is_none());
}
}