712 lines
23 KiB
Plaintext
Raw Normal View History

# Interactive Setup Wizard Module
# Provides step-by-step interactive guidance for system setup
# Follows Nushell guidelines: explicit types, single purpose, no try-catch
# [command]
# name = "setup wizard"
# group = "configuration"
# tags = ["setup", "interactive", "wizard"]
# version = "3.0.0"
# requires = ["nushell:0.109.0"]
use ./mod.nu *
use ./detection.nu *
use ./validation.nu *
# ============================================================================
# INPUT HELPERS
# ============================================================================
# Helper to read one line of input in Nushell 0.109.1
# Reads directly from /dev/tty for TTY mode, handles piped input gracefully
def read-input-line [] {
# Try to read from /dev/tty first (TTY/interactive mode)
let read_result = (do { open /dev/tty | lines | first | str trim } | complete)
# If /dev/tty worked, return the line
if $read_result.exit_code == 0 {
($read_result.stdout)
} else {
# No /dev/tty (Windows, containers, or piped mode)
# Return empty string - this will use defaults in calling code
""
}
}
# Prompt user for simple yes/no question
export def prompt-yes-no [
question: string
] {
print ""
print -n ($question + " (y/n): ")
let response = (read-input-line)
if ($response | is-empty) { false } else { ($response == "y" or $response == "Y") }
}
# Prompt user for text input
export def prompt-text [
question: string
default_value: string = ""
] {
print ""
if ($default_value != "") {
print ($question + " [" + $default_value + "]: ")
} else {
print ($question + ": ")
}
let response = (read-input-line)
if ($response == "") {
$default_value
} else {
$response
}
}
# Prompt user for selection from list
export def prompt-select [
question: string
options: list<string>
] {
print ""
print $question
let option_count = ($options | length)
for i in (0..($option_count - 1)) {
let option = ($options | get $i)
let num = (($i + 1) | into string)
print (" " + $num + ") " + $option)
}
print ""
let count_str = ($option_count | into string)
print ("Select option (1-" + $count_str + "): ")
let choice_str = (read-input-line)
let choice = ($choice_str | into int | default 1)
if ($choice >= 1 and $choice <= $option_count) {
$options | get ($choice - 1)
} else {
$options | get 0
}
}
# Prompt user for number with validation
export def prompt-number [
question: string
min_value: int = 1
max_value: int = 1000
default_value: int = 0
] {
mut result = $default_value
mut valid = false
while (not $valid) {
print ""
if ($default_value != 0) {
print $"$question [$default_value]: "
} else {
print $"$question: "
}
let input_value = (read-input-line)
if ($input_value == "") {
$result = $default_value
$valid = true
} else {
let parsed = (do { $input_value | into int } | complete)
if ($parsed.exit_code == 0) {
let num = ($parsed.stdout | str trim | into int)
if ($num >= $min_value and $num <= $max_value) {
$result = $num
$valid = true
} else {
print-setup-warning $"Please enter a number between ($min_value) and ($max_value)"
}
} else {
print-setup-warning "Please enter a valid number"
}
}
}
$result
}
# ============================================================================
# PROFILE SELECTION
# ============================================================================
# Prompt for setup profile selection
export def prompt-profile-selection [] {
print ""
print-setup-header "Profile Selection"
print ""
print "Choose a setup profile for your provisioning system:"
print ""
print " 1) Developer - Fast local setup (<5 min, Docker Compose, minimal config)"
print " 2) Production - Full validated setup (Kubernetes/SSH, complete security, HA)"
print " 3) CI/CD - Ephemeral pipeline setup (automated, Docker Compose, cleanup)"
print ""
let options = ["Developer", "Production", "CI/CD"]
let choice = (prompt-select "Select profile" $options)
match $choice {
"Developer" => "developer"
"Production" => "production"
"CI/CD" => "cicd"
_ => "developer"
}
}
# ============================================================================
# SYSTEM CONFIGURATION PROMPTS
# ============================================================================
# Prompt for system configuration details
export def prompt-system-config [] {
print-setup-header "System Configuration"
print ""
print "Let's configure your provisioning system. This will set up the base configuration."
print ""
let config_path = (get-config-base-path)
print-setup-info $"Configuration will be stored in: ($config_path)"
let use_defaults = (prompt-yes-no "Use recommended paths for your OS?")
let confirmed_path = if $use_defaults {
config_path
} else {
(prompt-text "Configuration base path" $config_path)
}
{
config_path: $confirmed_path
os_name: (detect-os)
cpu_count: (get-cpu-count)
memory_gb: (get-system-memory-gb)
}
}
# ============================================================================
# DEPLOYMENT MODE PROMPTS
# ============================================================================
# Prompt for deployment mode selection
export def prompt-deployment-mode [
detection_report: record
] {
print-setup-header "Deployment Mode Selection"
print ""
print "Choose how platform services will be deployed:"
print ""
mut options = []
let caps = $detection_report.capabilities
if ($caps.docker_available and $caps.docker_compose_available) {
$options = ($options | append "docker-compose (Local Docker)")
}
if $caps.kubectl_available {
$options = ($options | append "kubernetes (Kubernetes cluster)")
}
if $caps.ssh_available {
$options = ($options | append "remote-ssh (Remote SSH)")
}
if $caps.systemd_available {
$options = ($options | append "systemd (System services)")
}
if ($options | length) == 0 {
print-setup-error "No deployment methods available"
return "unknown"
}
let recommended = (recommend-deployment-mode $detection_report)
print ("Recommended: " + $recommended)
print ""
let selected = (prompt-select "Select deployment mode" $options)
match $selected {
"docker-compose (Local Docker)" => { "docker-compose" }
"kubernetes (Kubernetes cluster)" => { "kubernetes" }
"remote-ssh (Remote SSH)" => { "remote-ssh" }
"systemd (System services)" => { "systemd" }
_ => { "docker-compose" }
}
}
# ============================================================================
# PROVIDER CONFIGURATION PROMPTS
# ============================================================================
# Prompt for provider selection
export def prompt-providers [] {
print-setup-header "Provider Selection"
print ""
print "Which infrastructure providers do you want to use?"
print "(Select at least one)"
print ""
let available_providers = ["upcloud", "aws", "hetzner", "local"]
mut selected = []
for provider in $available_providers {
let use_provider = (prompt-yes-no $"Use ($provider)?")
if $use_provider {
$selected = ($selected | append $provider)
}
}
if ($selected | length) == 0 {
print-setup-info "At least 'local' provider is required"
["local"]
} else {
$selected
}
}
# ============================================================================
# RESOURCE CONFIGURATION PROMPTS
# ============================================================================
# Prompt for resource allocation
export def prompt-resource-allocation [
detection_report: record
] {
print-setup-header "Resource Allocation"
print ""
let current_cpus = $detection_report.system.cpu_count
let current_memory = $detection_report.system.memory_gb
let cpu_count = (prompt-number "Number of CPUs to allocate" 1 $current_cpus $current_cpus)
let memory_gb = (prompt-number "Memory in GB to allocate" 1 $current_memory $current_memory)
print ""
print $"✅ Allocated ($cpu_count) CPUs and ($memory_gb) GB memory"
{
cpu_count: $cpu_count
memory_gb: $memory_gb
}
}
# ============================================================================
# SECURITY CONFIGURATION PROMPTS
# ============================================================================
# Prompt for security settings
export def prompt-security-config [] {
print-setup-header "Security Configuration"
print ""
let enable_mfa = (prompt-yes-no "Enable Multi-Factor Authentication (MFA)?")
let enable_audit = (prompt-yes-no "Enable audit logging for all operations?")
let require_approval = (prompt-yes-no "Require approval for destructive operations?")
{
enable_mfa: $enable_mfa
enable_audit: $enable_audit
require_approval_for_destructive: $require_approval
}
}
# ============================================================================
# WORKSPACE CONFIGURATION PROMPTS
# ============================================================================
# Prompt for initial workspace creation
export def prompt-initial-workspace [] {
print-setup-header "Initial Workspace"
print ""
print "Create an initial workspace for your infrastructure?"
print ""
let create_workspace = (prompt-yes-no "Create workspace now?")
if not $create_workspace {
return {
create_workspace: false
name: ""
description: ""
}
}
let workspace_name = (prompt-text "Workspace name" "default")
let workspace_description = (prompt-text "Workspace description (optional)" "")
{
create_workspace: true
name: $workspace_name
description: $workspace_description
}
}
# ============================================================================
# COMPLETE SETUP WIZARD
# ============================================================================
# Run complete interactive setup wizard
export def run-setup-wizard [
--verbose = false
] {
# Check if running in TTY or piped mode
let tty_check = (do { open /dev/tty | null } | complete)
let is_interactive = ($tty_check.exit_code == 0)
if not $is_interactive {
# In non-TTY mode, switch to defaults automatically
print " Non-interactive mode detected. Using recommended defaults..."
print ""
return (run-setup-with-defaults)
}
# Force immediate output
print ""
print "╔═══════════════════════════════════════════════════════════════╗"
print "║ PROVISIONING SYSTEM SETUP WIZARD ║"
print "║ ║"
print "║ This wizard will guide you through setting up provisioning ║"
print "║ for your infrastructure automation needs. ║"
print "╚═══════════════════════════════════════════════════════════════╝"
print ""
# Step 1: Environment Detection
print-setup-header "Step 1: Environment Detection"
print "Analyzing your system configuration..."
print ""
# Use simplified detection to avoid hangs
let detection_report = {
system: {
os: (detect-os)
architecture: (detect-architecture)
hostname: "localhost"
current_user: (get-current-user)
cpu_count: 4
memory_gb: 8
disk_gb: 100
}
capabilities: (get-deployment-capabilities)
network: {
internet_connected: true
docker_port_available: true
orchestrator_port_available: true
control_center_port_available: true
kms_port_available: true
}
existing_config: (get-existing-config-summary)
platform_services: {
orchestrator_running: false
control_center_running: false
kms_running: false
}
timestamp: (date now)
}
if $verbose {
print-detection-report $detection_report
}
# Step 2: Profile Selection (NEW - determines setup approach)
print ""
let profile = (prompt-profile-selection)
print-setup-success $"Selected profile: ($profile)"
# Step 3: System Configuration
let system_config = (prompt-system-config)
# Step 5: Deployment Mode
let deployment_mode = (prompt-deployment-mode $detection_report)
print-setup-success $"Selected deployment mode: ($deployment_mode)"
# Step 6: Provider Selection
let providers = (prompt-providers)
print-setup-success $"Selected providers: ($providers | str join ', ')"
# Step 7: Resource Allocation
let resources = (prompt-resource-allocation $detection_report)
# Step 8: Security Settings
let security = (prompt-security-config)
# Step 9: Initial Workspace
let workspace = (prompt-initial-workspace)
# Summary
print ""
print-setup-header "Setup Summary"
print ""
print "Configuration Details:"
print $" Profile: ($profile)"
print $" Config Path: ($system_config.config_path)"
print $" OS: ($system_config.os_name)"
print $" Deployment Mode: ($deployment_mode)"
print $" Providers: ($providers | str join ', ')"
print $" CPUs: ($resources.cpu_count)"
print $" Memory: ($resources.memory_gb) GB"
print $" MFA Enabled: (if $security.enable_mfa { 'Yes' } else { 'No' })"
print $" Audit Logging: (if $security.enable_audit { 'Yes' } else { 'No' })"
print ""
let confirm = (prompt-yes-no "Proceed with this configuration?")
if not $confirm {
print-setup-warning "Setup cancelled"
return {
completed: false
profile: ""
system_config: {}
deployment_mode: ""
providers: []
resources: {}
security: {}
workspace: {}
}
}
print ""
print-setup-success "Configuration confirmed!"
print ""
{
completed: true
profile: $profile
system_config: $system_config
deployment_mode: $deployment_mode
providers: $providers
resources: $resources
security: $security
workspace: $workspace
timestamp: (date now)
}
}
# ============================================================================
# QUICK SETUP (AUTOMATED WITH DEFAULTS)
# ============================================================================
# Run setup with recommended defaults (no interaction)
export def run-setup-with-defaults [] {
print-setup-header "Quick Setup (Recommended Defaults)"
print ""
print "Configuring with system-recommended defaults..."
print ""
{
completed: true
system_config: {
config_path: (get-config-base-path)
os_name: (detect-os)
cpu_count: (get-cpu-count)
memory_gb: (get-system-memory-gb)
}
deployment_mode: "docker-compose"
providers: ["local"]
resources: {
cpu_count: (get-cpu-count)
memory_gb: (get-system-memory-gb)
}
security: {
enable_mfa: true
enable_audit: true
require_approval_for_destructive: true
}
workspace: {
create_workspace: true
name: "default"
description: "Default workspace"
}
timestamp: (date now)
}
}
# Run minimal setup (only required settings)
export def run-minimal-setup [] {
print-setup-header "Minimal Setup"
print ""
print "Configuring with minimal required settings..."
print ""
{
completed: true
system_config: {
config_path: (get-config-base-path)
os_name: (detect-os)
}
deployment_mode: "docker-compose"
providers: ["local"]
resources: {}
security: {
enable_mfa: false
enable_audit: true
require_approval_for_destructive: false
}
workspace: {
create_workspace: false
}
timestamp: (date now)
}
}
# ============================================================================
# TYPEDIALOG HELPER FUNCTIONS
# ============================================================================
# Run TypeDialog form via bash wrapper and return parsed result
# This pattern avoids TTY/input issues in Nushell's execution stack
def run-typedialog-form [
wrapper_script: string
--backend: string = "tui"
] {
# Check if the wrapper script exists
if not ($wrapper_script | path exists) {
print-setup-warning "TypeDialog wrapper not found. Using fallback prompts."
return {
success: false
error: "TypeDialog wrapper not available"
use_fallback: true
}
}
# Set backend environment variable
$env.TYPEDIALOG_BACKEND = $backend
# Run bash wrapper (handles TTY input properly)
let result = (do { bash $wrapper_script } | complete)
if $result.exit_code != 0 {
print-setup-error "TypeDialog wizard failed or was cancelled"
return {
success: false
error: $result.stderr
use_fallback: true
}
}
# Read the generated JSON file
let json_output = ($wrapper_script | path dirname | path join "generated" | path join ($wrapper_script | path basename | str replace ".sh" "-result.json"))
if not ($json_output | path exists) {
print-setup-warning "TypeDialog output not found. Using fallback."
return {
success: false
error: "Output file not found"
use_fallback: true
}
}
# Parse JSON output (no try-catch)
let parse_result = (do { open $json_output | from json } | complete)
if $parse_result.exit_code != 0 {
return {
success: false
error: "Failed to parse TypeDialog output"
use_fallback: true
}
}
let values = ($parse_result.stdout)
{
success: true
values: $values
use_fallback: false
}
}
# ============================================================================
# INTERACTIVE SETUP USING TYPEDIALOG
# ============================================================================
# Run setup wizard using TypeDialog - modern TUI experience
# Uses bash wrapper to handle TTY input properly
export def run-setup-wizard-interactive [
--backend: string = "tui"
] {
print ""
print "╔═══════════════════════════════════════════════════════════════╗"
print "║ PROVISIONING SYSTEM SETUP WIZARD (TypeDialog) ║"
print "║ ║"
print "║ This wizard will guide you through setting up provisioning ║"
print "║ for your infrastructure automation needs. ║"
print "╚═══════════════════════════════════════════════════════════════╝"
print ""
# Run the TypeDialog-based wizard via bash wrapper
let wrapper_script = "provisioning/core/shlib/setup-wizard-tty.sh"
let form_result = (run-typedialog-form $wrapper_script --backend $backend)
# If TypeDialog not available or failed, fall back to basic wizard
if (not $form_result.success or $form_result.use_fallback) {
print-setup-info "Falling back to basic interactive wizard..."
return (run-setup-wizard)
}
# Extract values from form results
let values = $form_result.values
# Collect selected providers
let providers = (
[]
| if ($values.providers?.upcloud? | default false) { append "upcloud" } else { . }
| if ($values.providers?.aws? | default false) { append "aws" } else { . }
| if ($values.providers?.hetzner? | default false) { append "hetzner" } else { . }
| if ($values.providers?.local? | default false) { append "local" } else { . }
)
# Ensure at least one provider
let providers_final = if ($providers | length) == 0 { ["local"] } else { $providers }
# Create workspace config
let workspace_final = {
create_workspace: ($values.workspace?.create_workspace? | default false)
name: ($values.workspace?.name? | default "default")
description: ($values.workspace?.description? | default "")
}
# Display summary
print ""
print-setup-header "Setup Summary"
print ""
print "Configuration Details:"
print $" Config Path: ($values.system_config?.config_path? | default (get-config-base-path))"
print $" Deployment Mode: ($values.deployment_mode? | default 'docker-compose')"
print $" Providers: ($providers_final | str join ', ')"
print $" CPUs: ($values.resources?.cpu_count? | default 4)"
print $" Memory: ($values.resources?.memory_gb? | default 8) GB"
print $" MFA Enabled: (if ($values.security?.enable_mfa? | default false) { 'Yes' } else { 'No' })"
print $" Audit Logging: (if ($values.security?.enable_audit? | default false) { 'Yes' } else { 'No' })"
print ""
print-setup-success "Configuration confirmed!"
print ""
{
completed: true
system_config: {
config_path: ($values.system_config?.config_path? | default (get-config-base-path))
os_name: (detect-os)
cpu_count: ($values.resources?.cpu_count? | default 4)
memory_gb: ($values.resources?.memory_gb? | default 8)
}
deployment_mode: ($values.deployment_mode? | default "docker-compose")
providers: $providers_final
resources: {
cpu_count: ($values.resources?.cpu_count? | default 4)
memory_gb: ($values.resources?.memory_gb? | default 8)
}
security: {
enable_mfa: ($values.security?.enable_mfa? | default true)
enable_audit: ($values.security?.enable_audit? | default true)
require_approval_for_destructive: ($values.security?.require_approval_for_destructive? | default true)
}
workspace: $workspace_final
timestamp: (date now)
}
}