# AGENTS.md - mvsep-rs Development Guide
## Project Overview
`mvsep-rs` is a CLI tool for MVSEP music separation API. It's a single-binary Rust application using `clap` for CLI argument parsing and `reqwest` for HTTP requests.
## Build, Lint, and Test Commands
### Build
```bash
cargo build # Debug build
cargo build --release # Release build
```
### Lint & Format
```bash
cargo check # Type check without full compilation
cargo clippy # Run lints
cargo fmt # Format code
cargo fmt --check # Check formatting without changes
```
### Test
```bash
cargo test # Run all tests
cargo test <name> # Run specific test
cargo run # Run the CLI (debug mode)
```
### Other
```bash
cargo doc # Generate documentation
cargo tree # Show dependency tree
cargo update # Update dependencies
```
## Code Style Guidelines
### General
- This is a single-file project (`src/main.rs`) - keep it that way unless complexity demands modules
- Use Rust edition 2021
- Avoid `unsafe` code unless absolutely necessary
### Imports
```rust
// Group std imports first, then external crates, then local imports
use std::path::PathBuf;
use std::time::Duration;
use clap::{Parser, Subcommand};
use reqwest::multipart;
use serde::{Deserialize, Serialize};
```
### Formatting
- Use 4 spaces for indentation
- Add blank lines between logical sections
- Use trailing commas in match arms and struct literals
- Maximum line length: 100 characters (soft limit)
### Naming Conventions
- **Structs/Enums**: PascalCase (`CliConfig`, `TaskStatus`)
- **Functions**: snake_case (`get_proxy`, `build_client`)
- **Variables**: snake_case (`proxy_host`, `output_dir`)
- **Constants**: SCREAMING_SNAKE_CASE (`CONFIG_FILE`, `VERSION`)
- **Fields in structs**: snake_case
- **Option/Result types**: Use clear names like `proxy_host: Option<String>`
### Error Handling
- Use `Result<T, Box<dyn std::error::Error>>` for async functions
- Use `?` operator for error propagation
- Provide meaningful error messages with context
- Use `unwrap_or_else` for fallible operations with custom error handling
```rust
// Good examples
let config = CliConfig::load();
std::fs::write(config_path, content)?;
config.save()?;
// With custom fallback
let host = std::env::var("PROXY_HOST")
.ok()
.or_else(|| config.proxy_host.clone())
.unwrap_or_else(|| "127.0.0.1".to_string());
```
### Types
- Use explicit types for function parameters and return values
- Use `i32` for CLI arguments and API IDs
- Use `String` for owned strings, `&str` for string slices
- Use `Option<T>` for optional values
### Struct Derives
- Derive `Debug`, `Deserialize`, `Serialize` for data structures
- Derive `Clone` when needed
- Derive `Default` for default-constructible types
```rust
#[derive(Debug, Deserialize, Serialize, Default)]
struct CliConfig {
api_token: Option<String>,
output_dir: Option<String>,
}
#[derive(Debug, Deserialize, Clone)]
struct TaskStatus {
status: String,
data: Option<TaskStatusData>,
}
```
### Async/Await
- Use `async fn` for asynchronous operations
- Use `tokio` with `full` features for runtime
- Prefer `.await` over blocking calls
### CLI Design (clap)
- Use `#[derive(Parser)]` and `#[derive(Subcommand)]`
- Add doc comments for CLI help text
- Use `#[arg(...)]` for argument attributes
- Group related options with clear names
```rust
#[derive(Parser)]
#[command(name = "mvsep-cli")]
#[command(about = "MVSEP CLI - Music separation tool")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Test connection to API
Test,
/// Create a separation task
Create {
/// Audio file path
file: String,
#[arg(short = 't', long = "sep-type")]
sep_type: i32,
},
}
```
### HTTP Client
- Use `reqwest` with `multipart` and `json` features
- Create reusable client with `build_client()` function
- Use `Client::builder()` for custom configuration
### Configuration
- Store config in `~/.mvsep_cli_config` (HOME directory)
- Use JSON for config serialization
- Load config lazily with `CliConfig::load()`
- Support environment variable overrides where appropriate
## Common Patterns
### Config Pattern
```rust
#[derive(Debug, Deserialize, Serialize, Default)]
struct CliConfig {
api_token: Option<String>,
// ... other fields
}
impl CliConfig {
fn load() -> Self {
let config_path = Self::config_path();
if config_path.exists() {
if let Ok(content) = std::fs::read_to_string(&config_path) {
if let Ok(config) = serde_json::from_str(&content) {
return config;
}
}
}
Self::default()
}
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let content = serde_json::to_string_pretty(self)?;
std::fs::write(Self::config_path(), content)?;
Ok(())
}
}
```
### Async Handler Pattern
```rust
async fn create_task(file: &str) -> Result<String, Box<dyn std::error::Error>> {
let client = build_client()?;
// ... implementation
Ok(result)
}
```
## Adding New Commands
1. Add variant to `Commands` enum with doc comment
2. Add variant handler in `main()` match statement
3. Use helper functions for complex logic
4. Test the command manually before committing
## Dependencies
Key dependencies (see `Cargo.toml`):
- `clap` (v4) - CLI parsing with derive
- `reqwest` (v0.12) - HTTP client with multipart/json
- `tokio` (v1) - Async runtime
- `serde` + `serde_json` - Serialization