provisioning/docs/src/architecture/adr/adr-013-typdialog-integration.md
2026-01-08 21:22:57 +00:00

19 KiB

ADR-013: Typdialog Web UI Backend Integration for Interactive Configuration

Status

Accepted - 2025-01-08

Context

The provisioning system requires interactive user input for configuration workflows, workspace initialization, credential setup, and guided deployment scenarios. The system architecture combines Rust (performance-critical), Nushell (scripting), and Nickel (declarative configuration), creating challenges for interactive form-based input and multi-user collaboration.

The Interactive Configuration Problem

Current limitations:

  1. Nushell CLI: Terminal-only interaction

    • input command: Single-line text prompts only
    • No form validation, no complex multi-field forms
    • Limited to single-user, terminal-bound workflows
    • User experience: Basic and error-prone
  2. Nickel: Declarative configuration language

    • Cannot handle interactive prompts (by design)
    • Pure evaluation model (no side effects)
    • Forms must be defined statically, not interactively
    • No runtime user interaction
  3. Existing Solutions: Inadequate for modern infrastructure provisioning

    • Shell-based prompts: Error-prone, no validation, single-user
    • Custom web forms: High maintenance, inconsistent UX
    • Separate admin panels: Disconnected from IaC workflow
    • Terminal-only TUI: Limited to SSH sessions, no collaboration

Use Cases Requiring Interactive Input

  1. Workspace Initialization:

    # Current: Error-prone prompts
    let workspace_name = input "Workspace name: "
    let provider = input "Provider (aws/azure/oci): "
    # No validation, no autocomplete, no guidance
    
  2. Credential Setup:

    # Current: Insecure and basic
    let api_key = input "API Key: "  # Shows in terminal history
    let region = input "Region: "    # No validation
    
  3. Configuration Wizards:

    • Database connection setup (host, port, credentials, SSL)
    • Network configuration (CIDR blocks, subnets, gateways)
    • Security policies (encryption, access control, audit)
  4. Guided Deployments:

    • Multi-step infrastructure provisioning
    • Service selection with dependencies
    • Environment-specific overrides

Requirements for Interactive Input System

  • Terminal UI widgets: Text input, password, select, multi-select, confirm
  • Validation: Type checking, regex patterns, custom validators
  • Security: Password masking, sensitive data handling
  • User Experience: Arrow key navigation, autocomplete, help text
  • Composability: Chain multiple prompts into forms
  • Error Handling: Clear validation errors, retry logic
  • Rust Integration: Native Rust library (no subprocess overhead)
  • Cross-Platform: Works on Linux, macOS, Windows

Decision

Integrate typdialog with its Web UI backend as the standard interactive configuration interface for the provisioning platform. The major achievement of typdialog is not the TUI - it is the Web UI backend that enables browser-based forms, multi-user collaboration, and seamless integration with the provisioning orchestrator.

Architecture Diagram

┌─────────────────────────────────────────┐
│   Nushell Script                        │
│                                         │
│   provisioning workspace init           │
│   provisioning config setup             │
│   provisioning deploy guided            │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│   Rust CLI Handler                      │
│   (provisioning/core/cli/)              │
│                                         │
│   - Parse command                       │
│   - Determine if interactive needed     │
│   - Invoke TUI dialog module            │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│   TUI Dialog Module                     │
│   (typdialog wrapper)                   │
│                                         │
│   - Form definition (validation rules)  │
│   - Widget rendering (text, select)     │
│   - User input capture                  │
│   - Validation execution                │
│   - Result serialization (JSON/TOML)    │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│   typdialog Library                     │
│                                         │
│   - Terminal rendering (crossterm)      │
│   - Event handling (keyboard, mouse)    │
│   - Widget state management             │
│   - Input validation engine             │
└────────────┬────────────────────────────┘
             │
             ▼
┌─────────────────────────────────────────┐
│   Terminal (stdout/stdin)               │
│                                         │
│   ✅ Rich TUI with validation           │
│   ✅ Secure password input              │
│   ✅ Guided multi-step forms            │
└─────────────────────────────────────────┘

Implementation Characteristics

CLI Integration Provides:

  • Native Rust commands with TUI dialogs
  • Form-based input for complex configurations
  • Validation rules defined in Rust (type-safe)
  • Secure input (password masking, no history)
  • Error handling with retry logic
  • Serialization to Nickel/TOML/JSON

TUI Dialog Library Handles:

  • Terminal UI rendering and event loop
  • Widget management (text, select, checkbox, confirm)
  • Input validation and error display
  • Navigation (arrow keys, tab, enter)
  • Cross-platform terminal compatibility

Rationale

Why TUI Dialog Integration Is Required

Aspect Shell Prompts (current) Web Forms TUI Dialog (chosen)
User Experience Basic text only Rich UI Rich TUI
Validation Manual, error-prone Built-in Built-in
Security Plain text, history ⚠️ Network risk Secure terminal
Setup Complexity None Server required Minimal
Terminal Workflow Native Browser switch Native
Offline Support Always Requires server Always
Dependencies None Web stack Single crate
Error Handling Manual ⚠️ Complex Built-in retry

The Nushell Limitation

Nushell's input command is limited:

# Current: No validation, no security
let password = input "Password: "  # ❌ Shows in terminal
let region = input "AWS Region: "   # ❌ No autocomplete/validation

# Cannot do:
# - Multi-select from options
# - Conditional fields (if X then ask Y)
# - Password masking
# - Real-time validation
# - Autocomplete/fuzzy search

The Nickel Constraint

Nickel is declarative and cannot prompt users:

# Nickel defines what the config looks like, NOT how to get it
{
  database = {
    host | String,
    port | Number,
    credentials | { username: String, password: String },
  }
}

# Nickel cannot:
# - Prompt user for values
# - Show interactive forms
# - Validate input interactively

Why Rust + TUI Dialog Is The Solution

Rust provides:

  • Native terminal control (crossterm, termion)
  • Type-safe form definitions
  • Validation rules as functions
  • Secure memory handling (password zeroization)
  • Performance (no subprocess overhead)

TUI Dialog provides:

  • Widget library (text, select, multi-select, confirm)
  • Event loop and rendering
  • Validation framework
  • Error display and retry logic

Integration enables:

  • Nushell calls Rust CLI → Shows TUI dialog → Returns validated config
  • Nickel receives validated config → Type checks → Merges with defaults

Consequences

Positive

  • User Experience: Professional TUI with validation and guidance
  • Security: Password masking, sensitive data protection, no terminal history
  • Validation: Type-safe rules enforced before config generation
  • Developer Experience: Reusable form components across CLI commands
  • Error Handling: Clear validation errors with retry options
  • Offline First: No network dependencies for interactive input
  • Terminal Native: Fits CLI workflow, no context switching
  • Maintainability: Single library for all interactive input

Negative

  • Terminal Dependency: Requires interactive terminal (not scriptable)
  • Learning Curve: Developers must learn TUI dialog patterns
  • Library Lock-in: Tied to specific TUI library API
  • Testing Complexity: Interactive tests require terminal mocking
  • Non-Interactive Fallback: Need alternative for CI/CD and scripts

Mitigation Strategies

Non-Interactive Mode:

// Support both interactive and non-interactive
if terminal::is_interactive() {
    // Show TUI dialog
    let config = show_workspace_form()?;
} else {
    // Use config file or CLI args
    let config = load_config_from_file(args.config)?;
}

Testing:

// Unit tests: Test form validation logic (no TUI)
#[test]
fn test_validate_workspace_name() {
    assert!(validate_name("my-workspace").is_ok());
    assert!(validate_name("invalid name!").is_err());
}

// Integration tests: Use mock terminal or config files

Scriptability:

# Batch mode: Provide config via file
provisioning workspace init --config workspace.toml

# Interactive mode: Show TUI dialog
provisioning workspace init --interactive

Documentation:

  • Form schemas documented in docs/
  • Config file examples provided
  • Screenshots of TUI forms in guides

Alternatives Considered

Alternative 1: Shell-Based Prompts (Current State)

Pros: Simple, no dependencies Cons: No validation, poor UX, security risks Decision: REJECTED - Inadequate for production use

Alternative 2: Web-Based Forms

Pros: Rich UI, well-known patterns Cons: Requires server, network dependency, context switch Decision: REJECTED - Too complex for CLI tool

Alternative 3: Custom TUI Per Use Case

Pros: Tailored to each need Cons: High maintenance, code duplication, inconsistent UX Decision: REJECTED - Not sustainable

Alternative 4: External Form Tool (dialog, whiptail)

Pros: Mature, cross-platform Cons: Subprocess overhead, limited validation, shell escaping issues Decision: REJECTED - Poor Rust integration

Alternative 5: Text-Based Config Files Only

Pros: Fully scriptable, no interactive complexity Cons: Steep learning curve, no guidance for new users Decision: REJECTED - Poor user onboarding experience

Implementation Details

Form Definition Pattern

use typdialog::Form;

pub fn workspace_initialization_form() -> Result<WorkspaceConfig> {
    let form = Form::new("Workspace Initialization")
        .add_text_input("name", "Workspace Name")
            .required()
            .validator(|s| validate_workspace_name(s))
        .add_select("provider", "Cloud Provider")
            .options(&["aws", "azure", "oci", "local"])
            .required()
        .add_text_input("region", "Region")
            .default("us-west-2")
            .validator(|s| validate_region(s))
        .add_password("admin_password", "Admin Password")
            .required()
            .min_length(12)
        .add_confirm("enable_monitoring", "Enable Monitoring?")
            .default(true);

    let responses = form.run()?;

    // Convert to strongly-typed config
    let config = WorkspaceConfig {
        name: responses.get_string("name")?,
        provider: responses.get_string("provider")?.parse()?,
        region: responses.get_string("region")?,
        admin_password: responses.get_password("admin_password")?,
        enable_monitoring: responses.get_bool("enable_monitoring")?,
    };

    Ok(config)
}

Integration with Nickel

// 1. Get validated input from TUI dialog
let config = workspace_initialization_form()?;

// 2. Serialize to TOML/JSON
let config_toml = toml::to_string(&config)?;

// 3. Write to workspace config
fs::write("workspace/config.toml", config_toml)?;

// 4. Nickel merges with defaults
// nickel export workspace/main.ncl --format json
// (uses workspace/config.toml as input)

CLI Command Structure

// provisioning/core/cli/src/commands/workspace.rs

#[derive(Parser)]
pub enum WorkspaceCommand {
    Init {
        #[arg(long)]
        interactive: bool,

        #[arg(long)]
        config: Option<PathBuf>,
    },
}

pub fn handle_workspace_init(args: InitArgs) -> Result<()> {
    if args.interactive || terminal::is_interactive() {
        // Show TUI dialog
        let config = workspace_initialization_form()?;
        config.save("workspace/config.toml")?;
    } else if let Some(config_path) = args.config {
        // Use provided config
        let config = WorkspaceConfig::load(config_path)?;
        config.save("workspace/config.toml")?;
    } else {
        bail!("Either --interactive or --config required");
    }

    // Continue with workspace setup
    Ok(())
}

Validation Rules

pub fn validate_workspace_name(name: &str) -> Result<(), String> {
    // Alphanumeric, hyphens, 3-32 chars
    let re = Regex::new(r"^[a-z0-9-]{3,32}$").unwrap();
    if !re.is_match(name) {
        return Err("Name must be 3-32 lowercase alphanumeric chars with hyphens".into());
    }
    Ok(())
}

pub fn validate_region(region: &str) -> Result<(), String> {
    const VALID_REGIONS: &[&str] = &["us-west-1", "us-west-2", "us-east-1", "eu-west-1"];
    if !VALID_REGIONS.contains(&region) {
        return Err(format!("Invalid region. Must be one of: {}", VALID_REGIONS.join(", ")));
    }
    Ok(())
}

Security: Password Handling

use zeroize::Zeroizing;

pub fn get_secure_password() -> Result<Zeroizing<String>> {
    let form = Form::new("Secure Input")
        .add_password("password", "Password")
            .required()
            .min_length(12)
            .validator(password_strength_check);

    let responses = form.run()?;

    // Password automatically zeroized when dropped
    let password = Zeroizing::new(responses.get_password("password")?);

    Ok(password)
}

Testing Strategy

Unit Tests:

#[test]
fn test_workspace_name_validation() {
    assert!(validate_workspace_name("my-workspace").is_ok());
    assert!(validate_workspace_name("UPPERCASE").is_err());
    assert!(validate_workspace_name("ab").is_err()); // Too short
}

Integration Tests:

// Use non-interactive mode with config files
#[test]
fn test_workspace_init_non_interactive() {
    let config = WorkspaceConfig {
        name: "test-workspace".into(),
        provider: Provider::Local,
        region: "us-west-2".into(),
        admin_password: "secure-password-123".into(),
        enable_monitoring: true,
    };

    config.save("/tmp/test-config.toml").unwrap();

    let result = handle_workspace_init(InitArgs {
        interactive: false,
        config: Some("/tmp/test-config.toml".into()),
    });

    assert!(result.is_ok());
}

Manual Testing:

# Test interactive flow
cargo build --release
./target/release/provisioning workspace init --interactive

# Test validation errors
# - Try invalid workspace name
# - Try weak password
# - Try invalid region

Configuration Integration

CLI Flag:

# provisioning/config/config.defaults.toml
[ui]
interactive_mode = "auto"  # "auto" | "always" | "never"
dialog_theme = "default"   # "default" | "minimal" | "colorful"

Environment Override:

# Force non-interactive mode (for CI/CD)
export PROVISIONING_INTERACTIVE=false

# Force interactive mode
export PROVISIONING_INTERACTIVE=true

Documentation Requirements

User Guides:

  • docs/user/interactive-configuration.md - How to use TUI dialogs
  • docs/guides/workspace-setup.md - Workspace initialization with screenshots

Developer Documentation:

  • docs/development/tui-forms.md - Creating new TUI forms
  • Form definition best practices
  • Validation rule patterns

Configuration Schema:

# provisioning/schemas/workspace.ncl
{
  WorkspaceConfig = {
    name
      | doc "Workspace identifier (3-32 alphanumeric chars with hyphens)"
      | String,
    provider
      | doc "Cloud provider"
      | [| 'aws, 'azure, 'oci, 'local |],
    region
      | doc "Deployment region"
      | String,
    admin_password
      | doc "Admin password (min 12 characters)"
      | String,
    enable_monitoring
      | doc "Enable monitoring services"
      | Bool,
  }
}

Migration Path

Phase 1: Add Library

  • Add typdialog dependency to provisioning/core/cli/Cargo.toml
  • Create TUI dialog wrapper module
  • Implement basic text/select widgets

Phase 2: Implement Forms

  • Workspace initialization form
  • Credential setup form
  • Configuration wizard forms

Phase 3: CLI Integration

  • Update CLI commands to use TUI dialogs
  • Add --interactive / --config flags
  • Implement non-interactive fallback

Phase 4: Documentation

  • User guides with screenshots
  • Developer documentation for form creation
  • Example configs for non-interactive use

Phase 5: Testing

  • Unit tests for validation logic
  • Integration tests with config files
  • Manual testing on all platforms

References

  • typdialog Crate (or similar: dialoguer, inquire)
  • crossterm - Terminal manipulation
  • zeroize - Secure memory zeroization
  • ADR-004: Hybrid Architecture (Rust/Nushell integration)
  • ADR-011: Nickel Migration (declarative config language)
  • ADR-012: Nushell Plugins (CLI wrapper patterns)
  • Nushell input command limitations: Nushell Book - Input

Status: Accepted Last Updated: 2025-01-08 Implementation: Planned Priority: High (User onboarding and security) Estimated Complexity: Moderate