61 releases
Uses new Rust 2024
| 0.10.7 | Oct 31, 2025 |
|---|---|
| 0.10.5 | Jun 26, 2025 |
| 0.10.1 | Mar 19, 2025 |
| 0.9.6 | Dec 3, 2024 |
| 0.2.0 | Jul 26, 2023 |
#1 in #line-applications
1,135 downloads per month
Used in 2 crates
45KB
905 lines
starbase
Application framework for building performant command line applications and developer tools.
Usage
An application uses a session based approach, where a session object contains data required for the entire application lifecycle.
Create an App, optionally setup diagnostics (miette) and tracing (tracing), and then run the
application with the provided session. A mutable session is required, as the session can be mutated
for each phase.
use starbase::{App, MainResult};
use std::process::ExitCode;
use crate::CustomSession;
#[tokio::main]
async fn main() -> MainResult {
let app = App::default();
app.setup_diagnostics();
let exit_code = app.run(CustomSession::default(), |session| async {
// Run CLI
Ok(None)
}).await?;
Ok(ExitCode::from(exit_code))
}
Session
A session must implement the AppSession trait. This trait provides 4 optional methods, each
representing a different phase in the application life cycle.
use starbase::{AppSession, AppResult};
use std::path::PathBuf;
use async_trait::async_trait;
#[derive(Clone)]
pub struct CustomSession {
pub workspace_root: PathBuf,
}
#[async_trait]
impl AppSession for CustomSession {
async fn startup(&mut self) -> AppResult {
self.workspace_root = detect_workspace_root()?;
Ok(None)
}
}
Sessions must be cloneable and be
Send + Synccompatible. We clone the session when spawning tokio tasks. If you want to persist data across threads, wrap session properties inArc,RwLock, and other mechanisms.
Phases
An application is divided into phases, where each phase will be processed and completed before moving onto the next phase. The following phases are available:
- Startup - Register, setup, or load initial session state.
- Example: load configuration, detect workspace root, load plugins
- Analyze - Analyze the current environment, update state, and prepare for execution.
- Example: generate project graph, load cache, signin to service
- Execute - Execute primary business logic (
App#run).- Example: process dependency graph, run generator, check for new version
- Shutdown - Cleanup and shutdown on success of the entire lifecycle, or on failure of a
specific phase.
- Example: cleanup temporary files, shutdown server
If a session implements the
AppSession#executetrait method, it will run in parallel with theApp#runmethod.
How to
Error handling
Errors and diagnostics are provided by the miette crate. All
layers of the application return the miette::Result type (via AppResult). This allows for errors
to be easily converted to diagnostics, and for miette to automatically render to the terminal for
errors and panics.
To benefit from this, update your main function to return MainResult.
use starbase::{App, MainResult};
#[tokio::main]
async fn main() -> MainResult {
let app = App::default();
app.setup_diagnostics();
app.setup_tracing_with_defaults();
// ...
Ok(())
}
To make the most out of errors, and in turn diagnostics, it's best (also suggested) to use the
thiserror crate.
use miette::Diagnostic;
use thiserror::Error;
#[derive(Debug, Diagnostic, Error)]
pub enum AppError {
#[error(transparent)]
#[diagnostic(code(app::io_error))]
IoError(#[from] std::io::Error),
#[error("Systems offline!")]
#[diagnostic(code(app::bad_code))]
SystemsOffline,
}
Caveats
A returned Err must be converted to a diagnostic first. There are 2 approaches to achieve this:
#[system]
async fn could_fail() {
// Convert error using into()
Err(AppError::SystemsOffline.into())
// OR use ? operator on Err()
Err(AppError::SystemsOffline)?
}
Dependencies
~9–14MB
~238K SLoC