# Clarence
Build powerful CLI tools backed by HTTP APIs in Rust.
```rust
use clarence::Clarence;
fn main() {
Clarence::builder()
.name("mycli")
.base_url("https://api.example.com")
.run();
}
```
```bash
mycli users list --active
# → GET https://api.example.com/users/list?active=true
```
## Features
- **Automatic HTTP routing** - Commands map to API endpoints
- **Built-in authentication** - Store tokens, use in headers
- **Multi-environment support** - Optional dev, staging, prod configs
- **Git context** - Send repository info (working directory) as HTTP headers
- **Custom handlers** - Override default behavior
- **JSON CLI actions** - Let your API control the CLI (print, download, execute, storage)
## Quick Start
```sh
cargo add clarence
```
### Basic HTTP CLI
```rust
use clarence::Clarence;
fn main() {
Clarence::builder()
.name("mycli")
.base_url("https://api.example.com")
.help("My CLI tool")
.version(env!("CARGO_PKG_VERSION"))
.run();
}
```
Usage:
```bash
mycli deploy staging --force true
# → GET https://api.example.com/deploy/staging?force=true
```
### With Authentication
```rust
use clarence::{Clarence, Context, Handler, Result, StorageRef};
fn main() {
Clarence::builder()
.name("mycli")
.base_url("https://api.example.com")
.header("Authorization", StorageRef::get("token"))
.on("login", Handler::custom(login))
.fallback(Handler::http().build())
.run();
}
fn login(ctx: &mut Context) -> Result<i32> {
let token = ctx.args.get_flag("token")
.ok_or("Usage: mycli login --token YOUR_TOKEN")?;
ctx.store.set("token", format!("Bearer {}", token))?;
ctx.end().success_with("Logged in successfully")
}
```
Usage:
```bash
mycli login --token abc123
mycli users list
# → GET https://api.example.com/users/list (Authorization: Bearer abc123)
```
### Custom Handlers
```rust
use clarence::{Clarence, Context, Handler, Method, Result};
fn main() {
Clarence::builder()
.name("mycli")
.base_url("https://api.example.com")
.on("login", Handler::custom(login))
.on("deploy", Handler::http()
.method(Method::Post)
.build())
.run();
}
fn login(ctx: &mut Context) -> Result<i32> {
let user = ctx.args.positional.first().unwrap();
println!("Logging in as {}", user);
ctx.end().success()
}
```
### Multi-Environment
```rust
use clarence::{Clarence, Environment, StorageRef};
fn main() {
Clarence::builder()
.name("mycli")
.environments(
Environment::builder()
.flag("env")
.default("dev")
.add("prod", "https://api.prod.com", |env| {
env.header("X-Env", "production")
})
.add("dev", "http://localhost:3000", |_| {})
.build()
)
.run();
}
```
Usage:
```bash
mycli deploy # Uses dev (default)
mycli deploy --env prod # Uses prod
```
### Git Context
Send git repository information as HTTP headers:
```rust
use clarence::{Clarence, Handler, Method};
fn main() {
// Enable globally for all requests
Clarence::builder()
.name("mycli")
.base_url("https://api.example.com")
.send_git_context() // Adds x-cli-git-* headers
.run();
// Or enable per-handler
Clarence::builder()
.name("mycli")
.base_url("https://api.example.com")
.on("deploy", Handler::http()
.method(Method::Post)
.send_git_context() // Only for this handler
.build())
.run();
// Access in custom handlers
println!("Branch: {:?}", ctx.git.branch);
println!("Repo: {:?}", ctx.git.repo_name);
}
ctx.end().success()
})
}
```
HTTP headers sent:
- `x-cli-git: true` (or `false`)
- `x-cli-git-repo-url: https://github.com/user/repo.git`
- `x-cli-git-repo-name: repo`
- `x-cli-git-branch: main`
### JSON CLI Actions
Your API can control CLI behavior returning `cli_actions`:
```json
{
"cli_actions": [
{ "type": "print", "text": "✓ Deployed to production" },
{ "type": "execute", "command": "ls" },
{ "type": "download", "url": "https://...", "path": "./deploy.log" },
{ "type": "storage_set", "key": "last_deploy", "value": "prod" },
{ "type": "storage_unset", "key": "last_deploy" }
]
}
```
Enable in handler:
```rust
use clarence::{Handler, JsonCliActionsParser};
Handler::http()
.parser(
JsonCliActionsParser::new()
.allow_print()
.allow_execute() // (!) Allows commands to be executed remotely
.allow_download()
.allow_storage_set()
.allow_storage_unset()
)
.build()
```
## API Reference
### Builder
```rust
Clarence::builder()
.name("mycli") // Required
.base_url("https://api.example.com") // Required (unless only custom handlers)
.header("key", "value") // Static header
.header("key", StorageRef::get("k")) // Dynamic from storage
.send_git_context() // Send git info as headers
.help("Help text") // Enable --help
.version("1.0.0") // Enable --version
.on("command", handler) // Register command
.fallback(handler) // Fallback for unknown commands
.environments(config) // Multi-environment
.run() // Start CLI
```
### Handlers
```rust
// Custom function
// HTTP request
Handler::http()
.method(Method::Post) // GET, POST, PUT, DELETE, PATCH
.endpoint("/path") // Custom endpoint
.header("key", "value") // Add custom header
.parser(parser) // Custom response parser
.send_git_context() // Send git info as headers
.build()
```
### Context
```rust
fn handler(ctx: &mut Context) -> Result<i32> {
// Arguments
ctx.args.command // Vec<String>
ctx.args.positional // Vec<String>
ctx.args.flags // HashMap<String, String>
ctx.args.get_flag("key") // Option<&String>
ctx.args.has_flag("key") // bool
// Storage
ctx.store.set("key", value)?
ctx.store.get("key") // Option<String>
ctx.store.unset("key")?
// Git context
ctx.git.is_git_repo // bool
ctx.git.repo_url // Option<String>
ctx.git.repo_name // Option<String>
ctx.git.branch // Option<String>
// Exit helpers
ctx.end().success() // Exit 0
ctx.end().success_with("Done!") // Exit 0, print
ctx.end().error() // Exit 1
ctx.end().error_with("Failed!") // Exit 1, print error
ctx.end().code(42) // Custom exit code
Ok(0) // Or return directly
}
```
## Examples
Run examples with:
```bash
cargo run --example complete -- --help
cargo run --example github -- login --token YOUR_TOKEN
cargo run --example environments -- deploy --env prod
```
## Testing
```bash
cargo test
```
## License
MIT
## Repository
https://github.com/osscorplabs/clarence
## Authors
- Iñigo Taibo ([@itaibo](https://github.com/itaibo))