# hx-plugins
Plugin system with lifecycle hooks for hx.
## Overview
`hx-plugins` provides:
- **Lifecycle hooks** - Pre/post build, test, run, etc.
- **Shell scripts** - Simple script execution
- **Steel plugins** - Lisp-based scripted extensions
- **Custom commands** - User-defined CLI commands
## Quick Start
### Using Hooks in hx.toml
```toml
[plugins]
enabled = true
hook_timeout_ms = 5000
continue_on_error = false
[plugins.hooks]
pre_build = ["./scripts/lint.sh"]
post_build = ["./scripts/notify.sh"]
post_test = ["./scripts/coverage.sh"]
```
### Programmatic Usage
```rust
use hx_plugins::{PluginManager, PluginConfig, HookEvent, PluginContext};
// Create manager
let config = PluginConfig::from_manifest(&manifest);
let mut manager = PluginManager::new(config)?;
// Initialize and load plugins
manager.initialize()?;
manager.load_all(&project_root)?;
// Run hooks
let ctx = PluginContext::new(project_root, project_name);
let result = manager.run_hook(HookEvent::PreBuild, &ctx)?;
if !result.success {
eprintln!("Pre-build hook failed: {:?}", result.error);
}
```
## Hook Events
| `PreBuild` | Before `hx build` | - |
| `PostBuild` | After build | BuildContext |
| `PreTest` | Before `hx test` | - |
| `PostTest` | After tests | TestContext |
| `PreRun` | Before `hx run` | - |
| `PostRun` | After run | - |
| `PreClean` | Before `hx clean` | - |
| `PostClean` | After clean | - |
| `PreLock` | Before `hx lock` | - |
| `PostLock` | After lock | - |
| `Init` | After `hx init` | - |
## HookEvent
```rust
pub enum HookEvent {
PreBuild,
PostBuild,
PreTest,
PostTest,
PreRun,
PostRun,
PreClean,
PostClean,
PreLock,
PostLock,
Init,
}
impl HookEvent {
/// Get config key name (e.g., "pre_build")
pub fn config_key(&self) -> &'static str;
}
```
## Context Types
### PluginContext
```rust
pub struct PluginContext {
/// Project root directory
pub project_root: PathBuf,
/// Project name
pub project_name: String,
/// GHC version (if known)
pub ghc_version: Option<String>,
/// Build context (for post-build)
pub build: Option<BuildContext>,
/// Test context (for post-test)
pub test: Option<TestContext>,
}
```
### BuildContext
```rust
pub struct BuildContext {
/// Build succeeded
pub success: bool,
/// Build duration
pub duration: Duration,
/// Warning messages
pub warnings: Vec<String>,
/// Error messages
pub errors: Vec<String>,
}
```
### TestContext
```rust
pub struct TestContext {
/// All tests passed
pub passed: bool,
/// Number of passed tests
pub passed_count: usize,
/// Number of failed tests
pub failed_count: usize,
/// Number of skipped tests
pub skipped_count: usize,
/// Test duration
pub duration: Duration,
}
```
## HookResult
```rust
pub struct HookResult {
/// Hook succeeded
pub success: bool,
/// Execution duration
pub duration: Duration,
/// Error message (if failed)
pub error: Option<String>,
/// Hook output
pub output: String,
}
```
## Configuration
### PluginConfig
```rust
pub struct PluginConfig {
/// Enable plugin system
pub enabled: bool,
/// Hook execution timeout (ms)
pub hook_timeout_ms: u64,
/// Plugin search paths
pub paths: Vec<PathBuf>,
/// Continue on hook failure
pub continue_on_error: bool,
/// Hook scripts
pub scripts: PluginHookConfig,
}
```
### From Manifest
```rust
impl From<ManifestPlugins> for PluginConfig {
fn from(manifest: ManifestPlugins) -> Self;
}
```
## Shell Script Hooks
Simple shell scripts for common tasks:
```bash
#!/bin/bash
# scripts/pre-build.sh
echo "Running pre-build checks..."
# Lint check
hlint src/
# Format check
fourmolu --mode check src/
exit $?
```
Configure in hx.toml:
```toml
[plugins.hooks]
pre_build = ["./scripts/pre-build.sh"]
```
## Environment Variables
Hooks receive environment variables:
| `HX_PROJECT_ROOT` | Project directory |
| `HX_PROJECT_NAME` | Project name |
| `HX_GHC_VERSION` | GHC version |
| `HX_HOOK_EVENT` | Hook event name |
| `HX_BUILD_SUCCESS` | "true"/"false" (post-build) |
| `HX_TEST_PASSED` | "true"/"false" (post-test) |
## Plugin Manager
```rust
pub struct PluginManager {
config: PluginConfig,
engine: Option<SteelEngine>,
initialized: bool,
}
impl PluginManager {
/// Create with config
pub fn new(config: PluginConfig) -> Result<Self>;
/// Create with defaults
pub fn with_defaults() -> Result<Self>;
/// Get config reference
pub fn config(&self) -> &PluginConfig;
/// Initialize engine
pub fn initialize(&mut self) -> Result<()>;
/// Load all plugins from paths
pub fn load_all(&mut self, root: &Path) -> Result<()>;
/// Load single plugin
pub fn load_plugin(&mut self, path: &Path) -> Result<()>;
/// Run hook
pub fn run_hook(&mut self, event: HookEvent, ctx: &PluginContext) -> Result<HookResult>;
}
```
## Error Handling
```rust
pub enum PluginError {
/// Plugin file not found
NotFound(PathBuf),
/// Plugin load failed
LoadError(String),
/// Hook execution failed
ExecutionError(String),
/// Hook timed out
Timeout(Duration),
/// Plugin system not initialized
NotInitialized,
}
```
## CLI Integration (PluginHooks Helper)
For command implementations:
```rust
use crate::plugins::PluginHooks;
// In build command
let mut hooks = PluginHooks::from_project(&project, ghc_version);
if let Some(ref mut h) = hooks {
h.initialize()?;
}
// Pre-build
if let Some(ref mut h) = hooks {
if !h.run_pre_hook(HookEvent::PreBuild, output) {
return Ok(ExitCode::HookError.into());
}
}
// ... run build ...
// Post-build
if let Some(ref mut h) = hooks {
h.run_post_build_hook(success, duration, warnings, errors, output);
}
```
## Best Practices
1. **Keep hooks fast** - Don't block builds unnecessarily
2. **Use timeouts** - Set reasonable `hook_timeout_ms`
3. **Fail early** - Use `continue_on_error = false` for CI
4. **Idempotent scripts** - Hooks may run multiple times
5. **Exit codes matter** - Non-zero = failure
## Documentation
- [Steel API Reference](./steel-api.md) - Complete API reference for hx functions
- [Custom Commands](./custom-commands.md) - Creating custom hx commands