SEN: CLI Engine
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:
[]
= { = "0.1", = ["clap"] }
= { = "4", = ["derive"] }
= { = "1", = ["full"] }
Or use cargo add:
Example (Router API with Clap - Recommended)
use ;
use Parser;
// 1. Define application state
// 2. Define arguments with Clap derive macro
// These types are automatically parsed by SEN when clap feature is enabled
// Add descriptions to handlers with #[sen::handler] macro (Router API)
// Or use #[sen(desc = "...")] for Enum API (see below)
// 3. Implement handlers as async functions
// Handlers can accept State, Args, or both in any order
// Use #[sen::handler(desc = "...")] to add descriptions for help
// 4. Wire it up with Router (< 30 lines of main.rs)
async
// Set CLI metadata with #[sen::sen()] macro
Usage:
Hierarchical --help output (automatically generated):
myapp 1.0.0
My awesome CLI application
Usage: myapp [OPTIONS] <COMMAND>
Other Commands:
build Build the project
deploy Deploy to environment
status
Options:
-h, --help Print help
--help --json Show CLI schema (JSON format)
-V, --version Print version
Per-command --help (via Clap):
$ myapp build --help
Usage: cmd [OPTIONS]
Options:
--release Build in release mode
-j, --jobs <JOBS> Number of parallel jobs [default: 4]
-h, --help Print help
Example (Enum API with Clap - Type-safe alternative)
use ;
use Parser;
// 1. Define application state
// 2. Define arguments with Clap derive macro
// Same as Router API - just derive Parser on your argument types
// 3. Define commands with SenRouter derive macro
// This generates the execute() method and routing logic at compile-time
// Tell macro what State type to use
// The macro also generates Commands::help() for displaying all commands
// Example: println!("{}", Commands::help());
// 4. Implement handlers as async functions
// Same signature style as Router API
// 5. Wire it up (< 30 lines of main.rs)
async
Key Features of Enum API:
- Compile-time safety:
#[derive(SenRouter)]macro generates theexecute()method - Exhaustive matching: Compiler ensures all commands have handlers
- Clap integration: Just add
#[derive(Parser)]to argument types - Type-driven DI: Automatically injects
State<T>andArgs<T>based on handler signatures
๐ 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 = new
.route
.route
.with_state;
// Easy to integrate with existing CLIs
let response = router.execute.await;
Enum API - Compile-time safety:
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 pub async
3. Smart Error Handling
Errors automatically format with helpful hints:
Error: Invalid argument '--foo'
The value 'bar' is not supported.
Hint: Use one of: baz, qux
4. Professional Help Generation
Automatic hierarchical grouping - Commands are organized by prefix:
$ myctl --help
Configuration Commands:
edit Edit configuration in editor
init Initialize configuration file
show Show current configuration
Database Commands:
create Create a new database
delete Delete a database
list List all databases
Server Commands:
start Start server instances
stop Stop server instances
Clap integration - Per-command help with full argument details:
$ myctl db create --help
Usage: cmd [OPTIONS] <NAME>
Arguments:
<NAME> Database name
Options:
--size <SIZE> Storage size [default: 10GB]
--engine <ENGINE> Database engine [default: postgres]
-h, --help Print help
JSON schema export for AI agents and IDEs:
{
}
How grouping works:
- Commands with
:prefix are automatically grouped (e.g.,db:createโ "Database Commands") - Commands are displayed with just the suffix (e.g.,
createinstead ofdb:create) - Groups are sorted alphabetically, with "Other Commands" last
- Use
#[sen::handler(desc = "...")]to add descriptions
5. No Println! in Handlers
Handlers return structured data, framework handles output:
// โ Bad: Can't test, can't redirect
pub async
// โ
Good: Testable, flexible
pub async
๐ค 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 Router;
async
User runs:
How It Works
- Router detects
--agent-modeflag automatically - Strips the flag before passing args to handlers
- Sets
response.agent_mode = truefor your output logic - Zero boilerplate - no manual arg parsing needed
Example Output
Advanced: Manual Agent Mode
For complex scenarios with global options, you can still manually implement agent mode (see examples/practical-cli).
Features
- Automatic
--agent-modedetection: Framework handles flag parsing to_agent_json(): ConvertsResponseto structured JSON- Environment Sensors: Automatic collection of system metadata (requires
sensorsfeature) - Tier & Tags: Safety tier and command categorization metadata
- Structured Errors: Exit codes and error messages in machine-readable format
๐ก Argument Parsing: Clap Integration (Recommended)
SEN has built-in Clap integration - the de-facto standard for Rust CLI argument parsing.
๐ Use Clap (Recommended for 99% of CLIs)
Simply derive clap::Parser on your argument types:
Step 1: Enable the clap feature:
[]
= { = "0.1", = ["clap"] }
= { = "4", = ["derive"] }
Step 2: Define arguments with #[derive(Parser)]:
use ;
use Parser;
async
Step 3: Register the handler - that's it!
let router = new
.route
.with_state;
How it works: When the clap feature is enabled, SEN automatically implements FromArgs for any type implementing clap::Parser. Zero boilerplate required.
Benefits:
- โ
Automatic help generation (
--help) - โ Type-safe with compile-time validation
- โ Supports complex options (enums, lists, subcommands)
- โ Battle-tested (used by cargo, ripgrep, etc.)
- โ Recommended for all production CLIs
Example --help output (auto-generated from your #[arg] attributes):
All the documentation comments (///) in your struct become help text automatically!
Global Options (For CLI-wide Flags)
For applications with global flags that apply to all commands:
use FromGlobalArgs;
Use Global Options when:
- โ
You need flags that apply to all commands (
--verbose,--config) - โ
You want integration with
clapor other parsers - โ You have complex validation or conflicting flag logic
- โ
Building a production CLI (like
practical-cliexample)
Why practical-cli Uses Global Options
The practical-cli example intentionally uses FromGlobalArgs instead of FromArgs:
- Global flags:
--verboseand--configapply to all commands clapintegration: Usesclap::Commandfor help generation- Flexibility: Manual parsing allows complex validation
- Real-world pattern: Mirrors production CLI tools like
kubectl,docker, etc.
Key Insight: For complex CLIs with global flags, use FromGlobalArgs to parse them once, then use Clap's #[derive(Parser)] for per-command arguments.
See examples/practical-cli for a complete production-ready example showing:
- Global flags with
FromGlobalArgs - Per-command arguments with Clap's
#[derive(Parser)] - Nested routers for organizing commands by resource
Advanced: Manual FromArgs Implementation (Rarely Needed)
If you need custom parsing logic and cannot use Clap, you can manually implement FromArgs:
use ;
async
Only use manual FromArgs when:
- โ Clap doesn't support your use case (very rare)
- โ You need parsing logic that's impossible to express in Clap
- โ You're integrating with a non-Clap parser
For 99% of use cases, use Clap's #[derive(Parser)] instead.
๐ Plugin System (WASM)
SEN provides a secure, cross-platform plugin system powered by WebAssembly.
Why WASM Plugins?
- Write Once, Run Anywhere: Single
.wasmbinary works on all platforms - Language Agnostic: Write plugins in Rust, Zig, or any WASM-compatible language
- Secure by Default: Sandboxed execution with CPU/memory limits
- Hot Reload: Plugins reload automatically when files change
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ sen-plugin-api โ
โ Shared protocol types (MessagePack) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ โ
โโโโโโโโโโดโโโโโโโโโ โโโโโโโโโโดโโโโโโโโโ
โ sen-plugin-sdk โ โ sen-plugin-host โ
โ Rust SDK for โ โ Wasmtime-based โ
โ plugin authors โ โ plugin runtime โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Quick Start: Rust Plugin
use *;
;
export_plugin!;
Build with:
Quick Start: Zig Plugin
const sdk = @import("sdk/plugin.zig");
pub const plugin = sdk.Plugin{
.name = "echo",
.about = "Echoes arguments back",
.version = "1.0.0",
.args = &.{
.{ .name = "message", .description = "Message to echo" },
},
};
pub fn execute(ctx: *sdk.Context) sdk.Result {
var args = ctx.args();
const message = args.next() orelse "No message";
return sdk.Result.success(message);
}
comptime { sdk.exportPlugin(@This()); }
Build with:
Router Integration
use Router;
use ;
async
Hot Reload
use ;
let registry = new?;
let _watcher = new.await?;
// Plugins automatically reload when .wasm files change
Security Model
| Protection | Implementation |
|---|---|
| CPU Limit | 10M fuel per execution |
| Stack Limit | 1MB WASM stack |
| Memory Isolation | Per-plugin linear memory |
| API Versioning | Rejects incompatible plugins |
| Capabilities | Fine-grained permission system |
Permission System
Plugins declare required capabilities, and the host controls access:
use ;
// Choose a preset based on your environment
let config = interactive?; // Development
let config = ci?; // CI/CD
let config = strict?; // Production
// Or customize with builder
let config = new
.app_name
.strategy
.store
.prompt
.build?;
Trust Flags for CLI integration:
Available Strategies:
| Strategy | Behavior |
|---|---|
| Default | Prompts for ungranted permissions |
| Strict | Denies in non-interactive mode |
| Permissive | Allows non-network without prompt |
| CI | Never prompts, requires pre-granted |
| TrustAll | Bypasses all checks (dev only) |
Effect System (Async I/O)
Plugins run in a sandboxed WASM environment without direct network access. The Effect system allows plugins to request async operations from the host:
โโโโโโโโโโโโโโโ execute() โโโโโโโโโโโโ
โ Plugin โ โโโโโโโโโโโโโโโโโโ โ Host โ
โ โ โ Effect::HttpGet โ โ
โ โ โ (async) โ
โ โ resume(result) โ fetch โ
โ โ โโโโโโโโโโโโโโโโโโโ โ
โ โ โ Success(...) โ โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโ
Plugin side:
use *;
Available Effects:
| Effect | Description |
|---|---|
HttpGet |
HTTP GET request |
HttpPost |
HTTP POST request |
Sleep |
Delay execution |
Plugin Examples
See the examples directory:
examples/hello-plugin/- Manual WASM implementation (Rust)examples/greet-plugin/- SDK-based plugin (Rust)examples/echo-plugin-zig/- Zig SDK exampleexamples/file-reader-plugin/- WASI filesystem access (Rust)examples/env-reader-plugin-zig/- WASI environment access (Zig)examples/http-plugin/- Effect system HTTP demo (Rust)
๐๏ธ 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
--releaseflag) - Test command (with optional filter)
- Proper error handling
Run it:
๐งช Testing
# Run all tests
# Test specific crate
๐ Documentation
- DESIGN.md - Complete design document
๐ฃ๏ธ Roadmap
- Phase 1: Core framework (State, CliResult, IntoResponse)
- Phase 2: Macro system (#[derive(SenRouter)])
- Phase 3: WASM Plugin System
- Plugin loading with wasmtime
- Rust SDK (
sen-plugin-sdk) - Zig SDK
- Hot reload
- Router integration
- Capabilities & Permission system
- Audit logging
- WASI Preview 1 integration (fs, env, stdio)
- Effect system (host-side async I/O)
- Phase 4: Advanced features (ReloadableConfig, tracing)
- Phase 5: Developer experience (CLI generator, templates)
๐ค Contributing
Contributions welcome! Please read DESIGN.md to understand the architecture first.
๐ License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
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