ANW-10 bootstrap anwesen crate and CLI skeleton
This commit is contained in:
parent
0c21e6c5ff
commit
317667cce7
9 changed files with 3099 additions and 0 deletions
152
src/cli.rs
Normal file
152
src/cli.rs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
//! CLI surface for the `anwesen` binary.
|
||||
//!
|
||||
//! Mirrors the contract pinned in the User Manual:
|
||||
//!
|
||||
//! ```text
|
||||
//! anwesen serve --vault <path> [--bind <addr:port>] [--log-level <level>]
|
||||
//! anwesen doctor --vault <path> [--log-level <level>]
|
||||
//! anwesen version
|
||||
//! ```
|
||||
//!
|
||||
//! `--bind` is `serve`-only; `doctor` does not bind a port; `version` takes
|
||||
//! no flags. Each flag has a matching `ANWESEN_<UPPER>` environment variable
|
||||
//! and CLI wins over env per the manual.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "anwesen", version, about, long_about = None)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
pub command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Run the read-only HTTP daemon.
|
||||
Serve(ServeArgs),
|
||||
/// Walk the vault once and report ingestion blockers. Read-only.
|
||||
Doctor(DoctorArgs),
|
||||
/// Print the version and exit.
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct ServeArgs {
|
||||
/// Path to the vault root.
|
||||
#[arg(long, env = "ANWESEN_VAULT")]
|
||||
pub vault: PathBuf,
|
||||
|
||||
/// Listen address for the HTTP server.
|
||||
#[arg(long, env = "ANWESEN_BIND", default_value = "127.0.0.1:8080")]
|
||||
pub bind: String,
|
||||
|
||||
/// Log verbosity.
|
||||
#[arg(long, env = "ANWESEN_LOG_LEVEL", default_value = "info")]
|
||||
pub log_level: LogLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct DoctorArgs {
|
||||
/// Path to the vault root.
|
||||
#[arg(long, env = "ANWESEN_VAULT")]
|
||||
pub vault: PathBuf,
|
||||
|
||||
/// Log verbosity.
|
||||
#[arg(long, env = "ANWESEN_LOG_LEVEL", default_value = "info")]
|
||||
pub log_level: LogLevel,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
#[value(rename_all = "lower")]
|
||||
pub enum LogLevel {
|
||||
Error,
|
||||
Warn,
|
||||
Info,
|
||||
Debug,
|
||||
Trace,
|
||||
}
|
||||
|
||||
impl LogLevel {
|
||||
pub fn as_filter_directive(self) -> &'static str {
|
||||
match self {
|
||||
Self::Error => "error",
|
||||
Self::Warn => "warn",
|
||||
Self::Info => "info",
|
||||
Self::Debug => "debug",
|
||||
Self::Trace => "trace",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
|
||||
Cli::try_parse_from(std::iter::once("anwesen").chain(args.iter().copied()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_requires_vault() {
|
||||
assert!(parse(&["serve"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serve_accepts_full_flag_set() {
|
||||
let cli = parse(&[
|
||||
"serve",
|
||||
"--vault",
|
||||
"/tmp/v",
|
||||
"--bind",
|
||||
"0.0.0.0:9000",
|
||||
"--log-level",
|
||||
"debug",
|
||||
])
|
||||
.expect("parse");
|
||||
match cli.command {
|
||||
Command::Serve(a) => {
|
||||
assert_eq!(a.vault, PathBuf::from("/tmp/v"));
|
||||
assert_eq!(a.bind, "0.0.0.0:9000");
|
||||
assert!(matches!(a.log_level, LogLevel::Debug));
|
||||
}
|
||||
_ => panic!("expected serve"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctor_rejects_bind() {
|
||||
// --bind is serve-only; doctor must not accept it.
|
||||
let err = parse(&["doctor", "--vault", "/tmp/v", "--bind", "0.0.0.0:9000"]).unwrap_err();
|
||||
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doctor_accepts_vault_and_log_level() {
|
||||
let cli = parse(&["doctor", "--vault", "/tmp/v", "--log-level", "warn"]).expect("parse");
|
||||
match cli.command {
|
||||
Command::Doctor(a) => {
|
||||
assert_eq!(a.vault, PathBuf::from("/tmp/v"));
|
||||
assert!(matches!(a.log_level, LogLevel::Warn));
|
||||
}
|
||||
_ => panic!("expected doctor"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_takes_no_flags() {
|
||||
assert!(matches!(
|
||||
parse(&["version"]).expect("parse").command,
|
||||
Command::Version
|
||||
));
|
||||
// version subcommand must reject any flag.
|
||||
assert!(parse(&["version", "--vault", "/tmp/v"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_subcommand_rejected() {
|
||||
assert!(parse(&["dance"]).is_err());
|
||||
}
|
||||
}
|
||||
49
src/main.rs
Normal file
49
src/main.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
//! Anwesen: read-only HTTP daemon over a markdown vault.
|
||||
//!
|
||||
//! This module wires the CLI to the (still-stub) `serve` and `doctor`
|
||||
//! subcommands. The full daemon arrives over [ANW-11..ANW-19].
|
||||
|
||||
mod cli;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
use crate::cli::{Cli, Command};
|
||||
|
||||
// Result is retained at the binary boundary per [[ADR-001 Language and
|
||||
// Foundation Libraries]] (anyhow at main); current stubs do not yet
|
||||
// surface errors.
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Command::Serve(args) => {
|
||||
init_logging(args.log_level);
|
||||
tracing::info!(
|
||||
vault = %args.vault.display(),
|
||||
bind = %args.bind,
|
||||
"anwesen serve: not yet implemented (ANW-10 stub)"
|
||||
);
|
||||
}
|
||||
Command::Doctor(args) => {
|
||||
init_logging(args.log_level);
|
||||
tracing::info!(
|
||||
vault = %args.vault.display(),
|
||||
"anwesen doctor: not yet implemented (ANW-10 stub)"
|
||||
);
|
||||
}
|
||||
Command::Version => {
|
||||
println!("{}", env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_logging(level: cli::LogLevel) {
|
||||
let filter = tracing_subscriber::EnvFilter::try_from_env("ANWESEN_LOG")
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level.as_filter_directive()));
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_writer(std::io::stderr)
|
||||
.try_init();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue