#![expect(
clippy::exit,
reason = "Allow exits because in this file we ideally handle all errors with known exit codes"
)]
use crate::history::changes;
use crate::server::app::serve_archive;
use crate::server::errors::CliError;
use crate::server::git::serve_git;
use crate::utils::archive::find_archive_path;
use clap::Parser;
use std::env;
use std::path::Path;
use std::path::PathBuf;
use std::process;
use tracing;
use tracing::Level;
use tracing_appender::rolling;
use tracing_subscriber::fmt;
use tracing_subscriber::fmt::writer::MakeWriterExt as _;
use tracing_subscriber::Layer as _;
use tracing_subscriber::{
filter::EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _,
};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long, default_value_t = String::from(".").to_owned())]
archive_path: String,
#[command(subcommand)]
subcommands: Subcommands,
}
#[derive(Clone, clap::Subcommand)]
enum Subcommands {
Git {
#[arg(short, long, default_value_t = 8080)]
port: u16,
},
Serve {
#[arg(short, long, default_value_t = 8080)]
port: u16,
#[arg(short, long, default_value_t = false)]
individual: bool,
},
Update {
#[arg(short = 'i', long = "include", num_args(1..))]
include: Vec<String>,
#[arg(short = 'e', long = "exclude", num_args(1..))]
exclude: Vec<String>,
},
}
#[expect(
clippy::expect_used,
reason = "Expect that console logging can be initialized"
)]
fn init_tracing(archive_path: &Path) {
let taf_dir = archive_path.join(PathBuf::from("./.taf"));
let debug_file_appender =
rolling::never(&taf_dir, "stelae-debug.log").with_max_level(Level::DEBUG);
let error_file_appender =
rolling::never(&taf_dir, "stelae-error.log").with_max_level(Level::WARN);
let mut debug_layer = fmt::layer().with_writer(debug_file_appender);
let mut error_layer = fmt::layer().with_writer(error_file_appender);
debug_layer = debug_layer.with_ansi(false);
error_layer = error_layer.with_ansi(false);
let console_layer = fmt::layer().with_target(true).with_filter(
EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.expect("Failed to initialize console logging"),
);
tracing_subscriber::registry()
.with(debug_layer)
.with(error_layer)
.with(console_layer)
.init();
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
}
#[expect(
clippy::pattern_type_mismatch,
reason = "Matching on a reference (&cli.subcommands) instead of by value; the match patterns borrow fields, which is intentional to avoid moving data."
)]
fn execute_command(cli: &Cli, archive_path: PathBuf) -> Result<(), CliError> {
match &cli.subcommands {
Subcommands::Git { port } => serve_git(&cli.archive_path, archive_path, *port),
Subcommands::Serve { port, individual } => {
serve_archive(&cli.archive_path, archive_path, *port, *individual)
}
Subcommands::Update { include, exclude } => {
changes::insert(&cli.archive_path, archive_path, include, exclude)
}
}
}
pub fn run() {
tracing::debug!("Starting application");
let cli = Cli::parse();
let archive_path_wd = Path::new(&cli.archive_path);
let Ok(archive_path) = find_archive_path(archive_path_wd) else {
tracing::error!(
"error: could not find `.taf` folder in `{}` or any parent directory",
&cli.archive_path
);
process::exit(1);
};
init_tracing(&archive_path);
match execute_command(&cli, archive_path) {
Ok(()) => process::exit(0),
Err(err) => {
tracing::error!("Application error: {err:?}");
process::exit(1);
}
}
}