sen 0.6.0

Script to System CLI Engine - A type-safe, macro-powered CLI framework
Documentation

SEN: CLI Engine

Rust License

A type-safe, macro-powered CLI framework.

๐ŸŽฏ Philosophy

SEN transforms CLI development from ad-hoc scripts into systematic applications with:

  • Compile-time safety: Enum-based routing with exhaustiveness checking
  • Zero boilerplate: Derive macros generate all wiring code
  • Type-driven DI: Handler parameters injected based on type signature
  • Fixed workflows: Predictable behavior for humans and AI agents
  • Strict separation: Prevents the "1000-line main.rs" problem

๐Ÿš€ Quick Start

Installation

Add to your Cargo.toml:

[dependencies]
sen = "0.1"

Or use cargo add:

cargo add sen

Example (Router API - Recommended)

use sen::{CliResult, State, Router};

// 1. Define application state
#[derive(Clone)]
pub struct AppState {
    pub config: String,
}

// 2. Implement handlers as async functions
mod handlers {
    use super::*;

    pub async fn status(state: State<AppState>) -> CliResult<String> {
        let app = state.read().await;
        Ok(format!("Config: {}", app.config))
    }

    pub async fn build(state: State<AppState>) -> CliResult<()> {
        println!("Building...");
        Ok(())
    }
}

// 3. Wire it up with Router (< 20 lines of main.rs)
#[tokio::main]
async fn main() {
    let state = AppState {
        config: "production".to_string(),
    };

    let router = Router::new()
        .route("status", handlers::status)
        .route("build", handlers::build)
        .with_state(state);

    let response = router.execute().await;

    if !response.output.is_empty() {
        println!("{}", response.output);
    }
    std::process::exit(response.exit_code);
}

Example (Enum API - Type-safe alternative)

use sen::{CliResult, State, SenRouter};

// 1. Define application state
pub struct AppState {
    pub config: String,
}

// 2. Define commands with derive macro
#[derive(SenRouter)]
#[sen(state = AppState)]
enum Commands {
    #[sen(handler = handlers::status)]
    Status,

    #[sen(handler = handlers::build)]
    Build(BuildArgs),
}

pub struct BuildArgs {
    pub release: bool,
}

// 3. Implement handlers as async functions
mod handlers {
    use super::*;

    pub async fn status(state: State<AppState>) -> CliResult<String> {
        let app = state.read().await;
        Ok(format!("Config: {}", app.config))
    }

    pub async fn build(state: State<AppState>, args: BuildArgs) -> CliResult<()> {
        let mode = if args.release { "release" } else { "debug" };
        println!("Building in {} mode", mode);
        Ok(())
    }
}

// 4. Wire it up (< 50 lines of main.rs)
#[tokio::main]
async fn main() {
    let state = State::new(AppState {
        config: "production".to_string(),
    });

    let cmd = Commands::parse(); // Your arg parsing logic
    let response = cmd.execute(state).await; // Macro-generated async execute!

    if !response.output.is_empty() {
        println!("{}", response.output);
    }
    std::process::exit(response.exit_code);
}

That's it! The #[derive(SenRouter)] macro generates the execute() method that:

  • Routes commands to handlers
  • Injects State<T> and args automatically
  • Converts results into responses with proper exit codes

๐Ÿ“ Project Structure

SEN enforces clean file separation from day one:

my-cli/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ main.rs              # Entry point only (< 50 lines)
โ”‚   โ”œโ”€โ”€ handlers/            # One file per command
โ”‚   โ”‚   โ”œโ”€โ”€ mod.rs
โ”‚   โ”‚   โ”œโ”€โ”€ status.rs
โ”‚   โ”‚   โ”œโ”€โ”€ build.rs
โ”‚   โ”‚   โ””โ”€โ”€ test.rs
โ”‚   โ”œโ”€โ”€ workflows/           # Multi-task operations
โ”‚   โ”‚   โ””โ”€โ”€ preflight.rs     # fmt โ†’ lint โ†’ test
โ”‚   โ”œโ”€โ”€ tasks/               # Atomic operations
โ”‚   โ”‚   โ”œโ”€โ”€ fmt.rs
โ”‚   โ”‚   โ””โ”€โ”€ lint.rs
โ”‚   โ””โ”€โ”€ lib.rs               # Re-exports

Why?

  • Each command is independently testable
  • No println! debugging (handlers return structured data)
  • Impossible to create "1000-line main.rs"
  • AI agents can understand and modify specific commands easily

๐ŸŽจ Key Features

1. Flexible Routing - Choose Your Style

Router API (Axum-style) - Dynamic and flexible:

// Register handlers dynamically
let router = Router::new()
    .route("status", handlers::status)
    .route("build", handlers::build)
    .with_state(app_state);

// Easy to integrate with existing CLIs
let response = router.execute(&args).await;

Enum API - Compile-time safety:

#[derive(SenRouter)]
#[sen(state = AppState)]
enum Commands {
    #[sen(handler = handlers::status)]  // Typo? Compile error!
    Status,
}

Both approaches are supported - choose based on your needs:

  • Router API: Better for gradual migration, dynamic routes, existing CLIs
  • Enum API: Better for new projects, compile-time exhaustiveness checking

2. Axum-Style Handler Signatures

// Order doesn't matter!
pub async fn handler1(state: State<App>, args: Args) -> CliResult<String>
pub async fn handler2(args: Args, state: State<App>) -> CliResult<String>

// State optional
pub async fn handler3(args: Args) -> CliResult<()>

3. Smart Error Handling

pub enum CliError {
    User(UserError),      // Exit code 1: user can fix
    System(SystemError),  // Exit code 101: bug/system failure
}

Errors automatically format with helpful hints:

Error: Invalid argument '--foo'

The value 'bar' is not supported.

Hint: Use one of: baz, qux

4. No Println! in Handlers

Handlers return structured data, framework handles output:

// โŒ Bad: Can't test, can't redirect
pub async fn status() -> CliResult<()> {
    println!("Status: OK");
    Ok(())
}

// โœ… Good: Testable, flexible
pub async fn status() -> CliResult<StatusReport> {
    Ok(StatusReport { status: "OK" })
}

๐Ÿค– Agent Mode (Machine-Readable Output)

SEN provides automatic AI agent integration through built-in --agent-mode flag support.

Automatic Agent Mode (Recommended)

Simply call .with_agent_mode() and the framework handles everything:

use sen::Router;

#[tokio::main]
async fn main() {
    let router = Router::new()
        .route("build", handlers::build)
        .with_agent_mode()  // Enable automatic --agent-mode support
        .with_state(state);

    let response = router.execute().await;

    // Automatically outputs JSON if --agent-mode was passed
    if response.agent_mode {
        println!("{}", response.to_agent_json());
    } else {
        if !response.output.is_empty() {
            println!("{}", response.output);
        }
    }

    std::process::exit(response.exit_code);
}

User runs:

myapp build              # Normal text output
myapp --agent-mode build # JSON output

How It Works

  1. Router detects --agent-mode flag automatically
  2. Strips the flag before passing args to handlers
  3. Sets response.agent_mode = true for your output logic
  4. Zero boilerplate - no manual arg parsing needed

Example Output

{
  "result": "success",
  "exit_code": 0,
  "output": "Build completed successfully",
  "tier": "safe",
  "tags": ["build", "production"],
  "sensors": {
    "timestamp": "2024-01-15T10:30:00Z",
    "os": "macos",
    "cwd": "/Users/user/project"
  }
}

Advanced: Manual Agent Mode

For complex scenarios with global options, you can still manually implement agent mode (see examples/practical-cli).

Features

  • Automatic --agent-mode detection: Framework handles flag parsing
  • to_agent_json(): Converts Response to structured JSON
  • Environment Sensors: Automatic collection of system metadata (requires sensors feature)
  • Tier & Tags: Safety tier and command categorization metadata
  • Structured Errors: Exit codes and error messages in machine-readable format

๐Ÿ’ก Argument Parsing: FromArgs vs Global Options

SEN provides multiple approaches for argument parsing, each suited for different use cases.

Simple Cases: Use FromArgs

For per-command arguments without global flags:

use sen::{Args, FromArgs, CliError};

#[derive(Debug)]
struct BuildArgs {
    release: bool,
}

impl FromArgs for BuildArgs {
    fn from_args(args: &[String]) -> Result<Self, CliError> {
        Ok(BuildArgs {
            release: args.contains(&"--release".to_string()),
        })
    }
}

async fn build(Args(args): Args<BuildArgs>) -> CliResult<String> {
    let mode = if args.release { "release" } else { "debug" };
    Ok(format!("Building in {} mode", mode))
}

Use FromArgs when:

  • โœ… You have simple per-command arguments
  • โœ… No global flags needed (--verbose, --config, etc.)
  • โœ… You want the framework to handle everything

Complex Cases: Use Global Options + Manual Parsing

For applications with global flags that apply to all commands:

use sen::FromGlobalArgs;

#[derive(Clone)]
pub struct GlobalOpts {
    pub verbose: bool,
    pub config_path: String,
}

impl FromGlobalArgs for GlobalOpts {
    fn from_global_args(args: &[String]) -> Result<(Self, Vec<String>), CliError> {
        let mut verbose = false;
        let mut config_path = "~/.config/myapp".to_string();
        let mut remaining_args = Vec::new();

        for arg in args {
            match arg.as_str() {
                "--verbose" | "-v" => verbose = true,
                "--config" => { /* handle next arg */ }
                _ => remaining_args.push(arg.clone()),
            }
        }

        Ok((GlobalOpts { verbose, config_path }, remaining_args))
    }
}

Use Global Options when:

  • โœ… You need flags that apply to all commands (--verbose, --config)
  • โœ… You want integration with clap or other parsers
  • โœ… You have complex validation or conflicting flag logic
  • โœ… Building a production CLI (like practical-cli example)

Why practical-cli Uses Global Options

The practical-cli example intentionally uses FromGlobalArgs instead of FromArgs:

  1. Global flags: --verbose and --config apply to all commands
  2. clap integration: Uses clap::Command for help generation
  3. Flexibility: Manual parsing allows complex validation
  4. Real-world pattern: Mirrors production CLI tools like kubectl, docker, etc.

Key Insight: FromArgs is a convenience feature, not required. For complex CLIs, manual parsing gives you full control.

See examples/practical-cli for a complete implementation.

๐Ÿ—๏ธ Architecture

SEN follows a three-layer design:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Router Layer (Compile-time)            โ”‚
โ”‚  - Enum-based command tree              โ”‚
โ”‚  - Handler binding via proc macros      โ”‚
โ”‚  - Type-safe routing                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Handler Layer (Runtime)                โ”‚
โ”‚  - Dependency injection (State, Args)   โ”‚
โ”‚  - Business logic execution             โ”‚
โ”‚  - Result<T, E> return type             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                   โ”‚
                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Response Layer (Exit)                  โ”‚
โ”‚  - Exit code mapping (0, 1, 101)        โ”‚
โ”‚  - Structured output (JSON/Human)       โ”‚
โ”‚  - Logging & telemetry                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

See DESIGN.md for full architecture details.

๐Ÿ“š Examples

Check out the examples/simple-cli directory for a working CLI with:

  • Status command (no args)
  • Build command (with --release flag)
  • Test command (with optional filter)
  • Proper error handling

Run it:

cd examples/simple-cli
cargo build
./target/debug/admin status
./target/debug/admin build --release
./target/debug/admin test my_test

๐Ÿงช Testing

# Run all tests
cargo test

# Test specific crate
cargo test -p sen
cargo test -p sen-rs-macros

๐Ÿ“– Documentation

๐Ÿ›ฃ๏ธ Roadmap

  • Phase 1: Core framework (State, CliResult, IntoResponse)
  • Phase 2: Macro system (#[derive(SenRouter)])
  • Phase 3: Advanced features (ReloadableConfig, tracing)
  • Phase 4: Developer experience (CLI generator, templates)

๐Ÿค Contributing

Contributions welcome! Please read DESIGN.md to understand the architecture first.

๐Ÿ“œ License

Licensed under either of:

at your option.

๐Ÿ™ Inspiration

SEN is inspired by:

  • Axum - Type-safe handler functions
  • Clap - CLI argument parsing
  • The philosophy of Anti-Fragility and Fixed Workflows

SEN (็ทš/ๅ…ˆ): The Line to Success, Leading the Future of CLI Development