provisioning/docs/src/architecture/adr/adr-012-nushell-nickel-plugin-cli-wrapper.md
2026-01-14 03:09:18 +00:00

11 KiB

ADR-014: Nushell Nickel Plugin - CLI Wrapper Architecture\n\n## Status\n\nAccepted - 2025-12-15\n\n## Context\n\nThe provisioning system integrates with Nickel for configuration management in advanced\nscenarios. Users need to evaluate Nickel files and work with their output in Nushell\nscripts. The nu_plugin_nickel plugin provides this integration.\n\nThe architectural decision was whether the plugin should:\n\n1. Implement Nickel directly using pure Rust (nickel-lang-core crate)\n2. Wrap the official Nickel CLI (nickel command)\n\n### System Requirements\n\nNickel configurations in provisioning use the module system:\n\n\n# config/database.ncl\nimport "lib/defaults" as defaults\nimport "lib/validation" as valid\n\n{\n databases: {\n primary = defaults.database & {\n name = "primary"\n host = "localhost"\n }\n }\n}\n\n\nModule system includes:\n\n- Import resolution with search paths\n- Standard library (builtins, stdlib packages)\n- Module caching\n- Complex evaluation context\n\n## Decision\n\nImplement the nu_plugin_nickel plugin as a CLI wrapper that invokes the external nickel command.\n\n### Architecture Diagram\n\n\n┌─────────────────────────────┐\n│ Nushell Script │\n│ │\n│ nickel-export json /file │\n│ nickel-eval /file │\n│ nickel-format /file │\n└────────────┬────────────────┘\n │\n ▼\n┌─────────────────────────────┐\n│ nu_plugin_nickel │\n│ │\n│ - Command handling │\n│ - Argument parsing │\n│ - JSON output parsing │\n│ - Caching logic │\n└────────────┬────────────────┘\n │\n ▼\n┌─────────────────────────────┐\n│ std::process::Command │\n│ │\n│ "nickel export /file ..." │\n└────────────┬────────────────┘\n │\n ▼\n┌─────────────────────────────┐\n│ Nickel Official CLI │\n│ │\n│ - Module resolution │\n│ - Import handling │\n│ - Standard library access │\n│ - Output formatting │\n│ - Error reporting │\n└────────────┬────────────────┘\n │\n ▼\n┌─────────────────────────────┐\n│ Nushell Records/Lists │\n│ │\n│ ✅ Proper types │\n│ ✅ Cell path access works │\n│ ✅ Piping works │\n└─────────────────────────────┘\n\n\n### Implementation Characteristics\n\nPlugin provides:\n\n- Nushell commands: nickel-export, nickel-eval, nickel-format, nickel-validate\n- JSON/YAML output parsing (serde_json → nu_protocol::Value)\n- Automatic caching (SHA256-based, ~80-90% hit rate)\n- Error handling (CLI errors → Nushell errors)\n- Type-safe output (nu_protocol::Value::Record, not strings)\n\nPlugin delegates to Nickel CLI:\n\n- Module resolution with search paths\n- Standard library access and discovery\n- Evaluation context setup\n- Module caching\n- Output formatting\n\n## Rationale\n\n### Why CLI Wrapper Is The Correct Choice\n\n| Aspect | Pure Rust (nickel-lang-core) | CLI Wrapper (chosen) |\n| -------- | ------------------------------- | ---------------------- |\n| Module resolution | Undocumented API | Official, proven |\n| Search paths | How to configure? | CLI handles it |\n| Standard library | How to access? | Automatic discovery |\n| Import system | API unclear | Built-in |\n| Evaluation context | Complex setup needed | CLI provides |\n| Future versions | ⚠️ Maintain parity | Automatic support |\n| Maintenance burden | 🔴 High | 🟢 Low |\n| Complexity | 🔴 High | 🟢 Low |\n| Correctness | ⚠️ Risk of divergence | Single source of truth |\n\n### The Module System Problem\n\nUsing nickel-lang-core directly would require the plugin to:\n\n1. Configure import search paths:\n\n rust\n // Where should Nickel look for modules?\n // Current directory? Workspace? System paths?\n // This is complex and configuration-dependent\n \n\n1. Access standard library:\n\n rust\n // Where is the Nickel stdlib installed?\n // How to handle different Nickel versions?\n // How to provide builtins?\n \n\n2. Manage module evaluation context:\n\n rust\n // Set up evaluation environment\n // Configure cache locations\n // Initialize type checker\n // This is essentially re-implementing CLI logic\n \n\n3. Maintain compatibility:\n - Every Nickel version change requires review\n - Risk of subtle behavioral differences\n - Duplicate bug fixes and features\n - Two implementations to maintain\n\n### Documentation Gap\n\nThe nickel-lang-core crate lacks clear documentation on:\n\n- How to configure import search paths\n- How to access standard library\n- How to set up evaluation context\n- What is the public API contract?\n\nThis makes direct usage risky. The CLI is the documented, proven interface.\n\n### Why Nickel Is Different From Simple Use Cases\n\nSimple use case (direct library usage works):\n\n- Simple evaluation with built-in functions\n- No external dependencies\n- No modules or imports\n\nNickel reality (CLI wrapper necessary):\n\n- Complex module system with search paths\n- External dependencies (standard library)\n- Import resolution with multiple fallbacks\n- Evaluation context that mirrors CLI\n\n## Consequences\n\n### Positive\n\n- Correctness: Module resolution guaranteed by official Nickel CLI\n- Reliability: No risk from reverse-engineering undocumented APIs\n- Simplicity: Plugin code is lean (~300 lines total)\n- Maintainability: Automatic tracking of Nickel changes\n- Compatibility: Works with all Nickel versions\n- User Expectations: Same behavior as CLI users experience\n- Community Alignment: Uses official Nickel distribution\n\n### Negative\n\n- External Dependency: Requires nickel binary installed in PATH\n- Process Overhead: ~100-200 ms per execution (heavily cached)\n- Subprocess Management: Spawn handling and stderr capture needed\n- Distribution: Provisioning must include Nickel binary\n\n### Mitigation Strategies\n\nDependency Management:\n\n- Installation scripts handle Nickel setup\n- Docker images pre-install Nickel\n- Clear error messages if nickel not found\n- Documentation covers installation\n\nPerformance:\n\n- Aggressive caching (80-90% typical hit rate)\n- Cache hits: ~1-5 ms (not 100-200 ms)\n- Cache directory: ~/.cache/provisioning/config-cache/\n\nDistribution:\n\n- Provisioning distributions include Nickel\n- Installers set up Nickel automatically\n- CI/CD has Nickel available\n\n## Alternatives Considered\n\n### Alternative 1: Pure Rust with nickel-lang-core\n\nPros: No external dependency\nCons: Undocumented API, high risk, maintenance burden\nDecision: REJECTED - Too risky\n\n### Alternative 2: Hybrid (Pure Rust + CLI fallback)\n\nPros: Flexibility\nCons: Adds complexity, dual code paths, confusing behavior\nDecision: REJECTED - Over-engineering\n\n### Alternative 3: WebAssembly Version\n\nPros: Standalone\nCons: WASM support unclear, additional infrastructure\nDecision: REJECTED - Immature\n\n### Alternative 4: Use Nickel LSP\n\nPros: Uses official interface\nCons: LSP not designed for evaluation, wrong abstraction\nDecision: REJECTED - Inappropriate tool\n\n## Implementation Details\n\n### Command Set\n\n1. nickel-export: Export/evaluate Nickel file\n\n nushell\n nickel-export json /path/to/file.ncl\n nickel-export yaml /path/to/file.ncl\n \n\n2. nickel-eval: Evaluate with automatic caching (for config loader)\n\n nushell\n nickel-eval /workspace/config.ncl\n \n\n3. nickel-format: Format Nickel files\n\n nushell\n nickel-format /path/to/file.ncl\n \n\n4. nickel-validate: Validate Nickel files/project\n\n nushell\n nickel-validate /path/to/project\n \n\n### Critical Implementation Detail: Command Syntax\n\nThe plugin uses the correct Nickel command syntax:\n\n\n// Correct:\ncmd.arg("export").arg(file).arg("--format").arg(format);\n// Results in: "nickel export /file --format json"\n\n// WRONG (previously):\ncmd.arg("export").arg(format).arg(file);\n// Results in: "nickel export json /file"\n// ↑ This triggers auto-import of nonexistent JSON module\n\n\n### Caching Strategy\n\nCache Key: SHA256(file_content + format)\nCache Hit Rate: 80-90% (typical provisioning workflows)\nPerformance:\n\n- Cache miss: ~100-200 ms (process fork)\n- Cache hit: ~1-5 ms (filesystem read + parse)\n- Speedup: 50-100x for cached runs\n\nStorage: ~/.cache/provisioning/config-cache/\n\n### JSON Output Processing\n\nPlugin correctly processes JSON output:\n\n1. Invokes: nickel export /file.ncl --format json\n2. Receives: JSON string from stdout\n3. Parses: serde_json::Value\n4. Converts: json_value_to_nu_value() (recursive)\n5. Returns: nu_protocol::Value::Record (not string!)\n\nThis enables Nushell cell path access:\n\n\nnickel-export json /config.ncl | .database.host # ✅ Works\n\n\n## Testing Strategy\n\nUnit Tests:\n\n- JSON parsing correctness\n- Value type conversions\n- Cache logic\n\nIntegration Tests:\n\n- Real Nickel file execution\n- Module imports verification\n- Search path resolution\n\nManual Verification:\n\n\n# Test module imports\nnickel-export json /workspace/config.ncl\n\n# Test cell path access\nnickel-export json /workspace/config.ncl | .database\n\n# Verify output types\nnickel-export json /workspace/config.ncl | type\n# Should show: record, not string\n\n\n## Configuration Integration\n\nPlugin integrates with provisioning config system:\n\n- Nickel path auto-detected: which nickel\n- Cache location: platform-specific cache_dir()\n- Errors: consistent with provisioning patterns\n\n## References\n\n- ADR-012: Nushell Plugins (general framework)\n- Nickel Official Documentation\n- nickel-lang-core Rust Crate\n- nu_plugin_nickel Implementation: provisioning/core/plugins/nushell-plugins/nu_plugin_nickel/\n- Related: ADR-013-NUSHELL-KCL-PLUGIN\n\n---\n\nStatus: Accepted and Implemented\nLast Updated: 2025-12-15\nImplementation: Complete\nTests: Passing