- Add `show-arguments` recipe documenting all version update commands - Add `complete-update-interactive` recipe for manual confirmations - Maintain `complete-update` as automatic mode (no prompts) - Update `update-help` to reference new recipes and modes - Document 7-step workflow and step-by-step differences Changes: - complete-update: Automatic mode (recommended for CI/CD) - complete-update-interactive: Interactive mode (with confirmations) - show-arguments: Complete documentation of all commands and modes - Both modes share same 7-step workflow with different behavior in Step 4
14 KiB
14 KiB
nu_plugin_orchestrator - Implementation Plan
Created: 2025-10-08 Status: Base structure complete, ready for implementation Pattern: Follows nu_plugin_tera exactly
Phase 1: Base Structure ✅ COMPLETE
Files Created
Cargo.toml- Package configuration with correct dependenciessrc/main.rs- Plugin entry point with 3 commandssrc/helpers.rs- Helper functions (placeholders)src/tests.rs- Test infrastructureREADME.md- Comprehensive documentationVERIFICATION.md- Structure verification
Commands Implemented (Placeholders)
orch status [--data-dir <path>]- Orchestrator status from local filesorch validate <workflow.k> [--strict]- Workflow validation locallyorch tasks [--status <status>] [--limit <n>]- Task listing from queue
Verification
cd provisioning/core/plugins/nushell-plugins/nu_plugin_orchestrator
cargo check # ✅ SUCCESS - 264 packages locked, compiling
Phase 2: Core Implementation (Next Steps)
2.1 File Reading - src/helpers.rs
read_local_status() Implementation
pub fn read_local_status(data_dir: &PathBuf) -> Result<OrchStatus, String> {
let status_file = data_dir.join("status.json");
if !status_file.exists() {
return Err(format!("Status file not found: {}", status_file.display()));
}
let content = std::fs::read_to_string(&status_file)
.map_err(|e| format!("Failed to read status file: {}", e))?;
serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse status JSON: {}", e))
}
Test Data: provisioning/platform/orchestrator/data/status.json
{
"running": true,
"tasks_pending": 5,
"tasks_running": 2,
"last_check": "2025-10-08T12:00:00Z"
}
read_task_queue() Implementation
use walkdir::WalkDir;
pub fn read_task_queue(
data_dir: &PathBuf,
status_filter: Option<String>,
) -> Result<Vec<TaskInfo>, String> {
let tasks_dir = data_dir.join("tasks");
if !tasks_dir.exists() {
return Ok(vec![]);
}
let mut tasks = Vec::new();
for entry in WalkDir::new(&tasks_dir)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "json"))
{
let content = std::fs::read_to_string(entry.path())
.map_err(|e| format!("Failed to read task file: {}", e))?;
let task: TaskInfo = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse task JSON: {}", e))?;
// Filter by status if provided
if let Some(ref filter) = status_filter {
if task.status != *filter {
continue;
}
}
tasks.push(task);
}
// Sort by priority (higher first), then by created_at (older first)
tasks.sort_by(|a, b| {
b.priority.cmp(&a.priority)
.then_with(|| a.created_at.cmp(&b.created_at))
});
Ok(tasks)
}
Test Data: provisioning/platform/orchestrator/data/tasks/task-001.json
{
"id": "task-001",
"status": "pending",
"created_at": "2025-10-08T12:00:00Z",
"priority": 5
}
2.2 Command Integration - src/main.rs
OrchStatus::run() Update
fn run(
&self,
_plugin: &OrchestratorPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: &Value,
) -> Result<Value, LabeledError> {
let data_dir = if let Some(dir_str) = call.get_flag::<String>("data-dir")? {
PathBuf::from(dir_str)
} else {
helpers::get_orchestrator_data_dir()
};
let status = helpers::read_local_status(&data_dir)
.map_err(|e| LabeledError::new(format!("Failed to read status: {}", e)))?;
Ok(Value::record(
record! {
"running" => Value::bool(status.running, call.head),
"tasks_pending" => Value::int(status.tasks_pending as i64, call.head),
"tasks_running" => Value::int(status.tasks_running as i64, call.head),
"last_check" => Value::string(status.last_check, call.head),
},
call.head,
))
}
OrchTasks::run() Update
fn run(
&self,
_plugin: &OrchestratorPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: &Value,
) -> Result<Value, LabeledError> {
let data_dir = helpers::get_orchestrator_data_dir();
let status_filter = call.get_flag::<String>("status")?;
let limit = call.get_flag::<i64>("limit")?;
let mut tasks = helpers::read_task_queue(&data_dir, status_filter)
.map_err(|e| LabeledError::new(format!("Failed to read tasks: {}", e)))?;
// Apply limit if provided
if let Some(n) = limit {
tasks.truncate(n as usize);
}
let task_values: Vec<Value> = tasks.into_iter().map(|task| {
Value::record(
record! {
"id" => Value::string(task.id, call.head),
"status" => Value::string(task.status, call.head),
"created_at" => Value::string(task.created_at, call.head),
"priority" => Value::int(task.priority as i64, call.head),
},
call.head,
)
}).collect();
Ok(Value::list(task_values, call.head))
}
2.3 Testing - src/tests.rs
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_data_dir_path() {
let dir = helpers::get_orchestrator_data_dir();
assert!(dir.to_string_lossy().contains("orchestrator/data"));
}
#[test]
fn test_read_status_file_not_found() {
let dir = tempdir().unwrap();
let result = helpers::read_local_status(&dir.path().to_path_buf());
assert!(result.is_err());
assert!(result.unwrap_err().contains("Status file not found"));
}
#[test]
fn test_read_status_valid() {
let dir = tempdir().unwrap();
let status_file = dir.path().join("status.json");
fs::write(&status_file, r#"{
"running": true,
"tasks_pending": 5,
"tasks_running": 2,
"last_check": "2025-10-08T12:00:00Z"
}"#).unwrap();
let status = helpers::read_local_status(&dir.path().to_path_buf()).unwrap();
assert_eq!(status.running, true);
assert_eq!(status.tasks_pending, 5);
assert_eq!(status.tasks_running, 2);
}
#[test]
fn test_read_task_queue_empty() {
let dir = tempdir().unwrap();
let tasks = helpers::read_task_queue(&dir.path().to_path_buf(), None).unwrap();
assert_eq!(tasks.len(), 0);
}
#[test]
fn test_read_task_queue_with_filter() {
let dir = tempdir().unwrap();
let tasks_dir = dir.path().join("tasks");
fs::create_dir(&tasks_dir).unwrap();
// Create test tasks
fs::write(tasks_dir.join("task-001.json"), r#"{
"id": "task-001",
"status": "pending",
"created_at": "2025-10-08T12:00:00Z",
"priority": 5
}"#).unwrap();
fs::write(tasks_dir.join("task-002.json"), r#"{
"id": "task-002",
"status": "running",
"created_at": "2025-10-08T12:01:00Z",
"priority": 3
}"#).unwrap();
// Filter by pending
let tasks = helpers::read_task_queue(
&dir.path().to_path_buf(),
Some("pending".to_string())
).unwrap();
assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].id, "task-001");
}
}
Phase 3: KCL Validation (Advanced)
3.1 Research KCL Validation
- Investigate
kcl-lang/kclRust bindings - Determine if we can call KCL parser directly
- Alternative: Shell out to
kclbinary
3.2 Implement validate_kcl_workflow()
pub fn validate_kcl_workflow(workflow_path: &str, strict: bool) -> Result<ValidationResult, String> {
// Option 1: Shell out to kcl binary
let output = std::process::Command::new("kcl")
.arg("vet")
.arg(workflow_path)
.output()
.map_err(|e| format!("Failed to run kcl: {}", e))?;
if output.status.success() {
Ok(ValidationResult {
valid: true,
errors: vec![],
warnings: vec![],
})
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(ValidationResult {
valid: false,
errors: vec![stderr.to_string()],
warnings: vec![],
})
}
}
3.3 Update OrchValidate::run()
fn run(
&self,
_plugin: &OrchestratorPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: &Value,
) -> Result<Value, LabeledError> {
let workflow: String = call.req(0)?;
let strict = call.has_flag("strict")?;
let result = helpers::validate_kcl_workflow(&workflow, strict)
.map_err(|e| LabeledError::new(format!("Validation failed: {}", e)))?;
Ok(Value::record(
record! {
"valid" => Value::bool(result.valid, call.head),
"errors" => Value::list(
result.errors.into_iter()
.map(|e| Value::string(e, call.head))
.collect(),
call.head
),
"warnings" => Value::list(
result.warnings.into_iter()
.map(|w| Value::string(w, call.head))
.collect(),
call.head
),
},
call.head,
))
}
Phase 4: Enhancements (Optional)
4.1 Caching
- Add in-memory cache for status.json (TTL: 1 second)
- Add cache invalidation on file modification
- Add
--no-cacheflag
4.2 Advanced Filtering
- Add date range filtering for tasks (
--from,--to) - Add priority range filtering (
--min-priority,--max-priority) - Add workflow path filtering (
--workflow)
4.3 Statistics
- Add
orch statscommand for task statistics - Group by status, priority, date
- Show average execution time
4.4 Monitoring
- Add
orch watchcommand for real-time updates - Use file system notifications (notify crate)
- Stream changes to stdout
Testing Strategy
Unit Tests (src/tests.rs)
- Test data directory path construction
- Test file reading with mock data
- Test error handling (missing files, invalid JSON)
- Test filtering and sorting logic
- Test KCL validation (if implemented)
Integration Tests (tests/integration_tests.rs)
- Test with real orchestrator data structure
- Test concurrent access to files
- Test performance with large datasets
- Test error messages are user-friendly
Plugin Tests (using nu-plugin-test-support)
- Test command signatures are correct
- Test command outputs match expected types
- Test error cases return proper LabeledError
Benchmarks (benches/benchmark.rs)
- Benchmark file reading performance
- Compare with REST API calls
- Measure memory usage
- Test with varying dataset sizes
Performance Targets
| Operation | Target | Baseline (REST API) | Improvement |
|---|---|---|---|
orch status |
<5ms | ~50ms | 10x |
orch tasks |
<10ms | ~30ms | 3x |
orch validate |
<20ms | ~100ms | 5x |
Installation & Usage
Build
cd provisioning/core/plugins/nushell-plugins
cargo build -p nu_plugin_orchestrator --release
Install
plugin add target/release/nu_plugin_orchestrator
plugin use orchestrator
Verify
# List commands
help commands | where name =~ "orch"
# Test status
orch status
# Test with custom data dir
orch status --data-dir ./test-data
# Test tasks
orch tasks
orch tasks --status pending
orch tasks --status pending --limit 10
# Test validation (once implemented)
orch validate workflows/example.k
orch validate workflows/example.k --strict
Success Criteria
Phase 1 (Complete) ✅
- Structure follows nu_plugin_tera pattern
- Cargo check passes
- 3 commands implemented (placeholders)
- README documentation complete
- Verification document created
Phase 2 (Core Implementation)
read_local_status()reads real filesread_task_queue()reads task directory- Commands return real data (not placeholders)
- Error handling is comprehensive
- Unit tests pass (>80% coverage)
Phase 3 (KCL Validation)
validate_kcl_workflow()validates KCL syntax- Validation errors are user-friendly
- Strict mode performs additional checks
- Integration with KCL tooling
Phase 4 (Production Ready)
- Performance targets met
- Integration tests pass
- Benchmarks show 5-10x improvement
- Documentation updated with real examples
- Plugin registered in project
Dependencies
Required
nu-plugin(0.107.1) - Plugin SDKnu-protocol(0.107.1) - Nushell typesserde(1.0) - Serializationserde_json(1.0) - JSON parsingchrono(0.4) - Timestampswalkdir(2.5) - Directory traversal
Optional (Phase 4)
notify- File system notificationskcl-lang- KCL validation (if Rust bindings available)criterion- Benchmarkingcache- In-memory caching
Risk Mitigation
Risk 1: Orchestrator Data Format Changes
Mitigation: Version check in status.json, graceful degradation
Risk 2: File Access Permissions
Mitigation: Clear error messages, suggest fixing permissions
Risk 3: Large Task Queues
Mitigation: Streaming, pagination, default limits
Risk 4: KCL Integration Complexity
Mitigation: Shell out to kcl binary as fallback
Timeline Estimate
- Phase 1 (Base Structure): ✅ Complete (1 hour)
- Phase 2 (Core Implementation): ~4 hours
- Phase 3 (KCL Validation): ~2 hours
- Phase 4 (Enhancements): ~4 hours
Total: ~11 hours (1 day of focused work)
Next Steps: Implement Phase 2 (Core Implementation) starting with read_local_status() in src/helpers.rs.