712 lines
23 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
}
}