feat: add interactive setup wizard for first-run configuration
Implement a comprehensive setup wizard to improve onboarding and configuration experience for both personal and server deployments. Key features: - Interactive wizard with profile selection (personal/server/custom) - Quick setup mode with sensible defaults - First-run detection with helpful welcome message - Directory configuration with validation - Server OAuth/JWT configuration with auto-generation - Environment file creation for server deployments - Template generators for systemd service and env files New commands: - opal setup # Interactive wizard - opal setup --quick # Quick setup with defaults - opal setup --profile # Use specific profile - opal setup --show-systemd # Show systemd template - opal setup --show-env # Show environment file template Implementation: - internal/wizard/prompts.go: Reusable prompt utilities - internal/wizard/profiles.go: Profile definitions and templates - cmd/setup.go: Main setup command implementation - cmd/root.go: First-run detection and welcome message - internal/engine/config.go: ConfigExists() and IsFirstRun() helpers User experience: - On first run, shows welcome message suggesting 'opal setup' - Non-intrusive - creates defaults automatically if skipped - Wizard guides through all configuration options - Server setup includes OAuth/JWT configuration - Environment file created with proper permissions (0600) - Clear next steps displayed after completion
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
package wizard
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PromptString prompts for a string value with a default
|
||||
func PromptString(prompt string, defaultValue string) string {
|
||||
if defaultValue != "" {
|
||||
fmt.Printf("%s [%s]: ", prompt, defaultValue)
|
||||
} else {
|
||||
fmt.Printf("%s: ", prompt)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// PromptPassword prompts for a password (hidden input would be nice, but we'll use simple input for now)
|
||||
func PromptPassword(prompt string) string {
|
||||
fmt.Printf("%s: ", prompt)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
return strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
// PromptBool prompts for a yes/no answer
|
||||
func PromptBool(prompt string, defaultValue bool) bool {
|
||||
defaultStr := "y/N"
|
||||
if defaultValue {
|
||||
defaultStr = "Y/n"
|
||||
}
|
||||
|
||||
fmt.Printf("%s [%s]: ", prompt, defaultStr)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(strings.ToLower(input))
|
||||
|
||||
if input == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return input == "y" || input == "yes"
|
||||
}
|
||||
|
||||
// PromptChoice prompts for a choice from a list
|
||||
func PromptChoice(prompt string, choices []string, defaultIndex int) int {
|
||||
fmt.Println(prompt)
|
||||
for i, choice := range choices {
|
||||
marker := " "
|
||||
if i == defaultIndex {
|
||||
marker = "●"
|
||||
} else {
|
||||
marker = "○"
|
||||
}
|
||||
fmt.Printf(" %s %d. %s\n", marker, i+1, choice)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Printf("Choice [%d]: ", defaultIndex+1)
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if input == "" {
|
||||
return defaultIndex
|
||||
}
|
||||
|
||||
choice, err := strconv.Atoi(input)
|
||||
if err != nil || choice < 1 || choice > len(choices) {
|
||||
fmt.Println("Invalid choice, using default")
|
||||
return defaultIndex
|
||||
}
|
||||
|
||||
return choice - 1
|
||||
}
|
||||
|
||||
// GenerateRandomSecret generates a random hex string for JWT secrets
|
||||
func GenerateRandomSecret(bytes int) (string, error) {
|
||||
b := make([]byte, bytes)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// PromptOrGenerate prompts for a value or generates one
|
||||
func PromptOrGenerate(prompt string, generator func() (string, error)) string {
|
||||
fmt.Printf("%s (press Enter to generate): ", prompt)
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
input, _ := reader.ReadString('\n')
|
||||
input = strings.TrimSpace(input)
|
||||
|
||||
if input == "" {
|
||||
generated, err := generator()
|
||||
if err != nil {
|
||||
fmt.Printf("Error generating: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
fmt.Printf("Generated: %s\n", generated)
|
||||
return generated
|
||||
}
|
||||
|
||||
return input
|
||||
}
|
||||
|
||||
// PrintHeader prints a formatted section header
|
||||
func PrintHeader(title string) {
|
||||
fmt.Println()
|
||||
fmt.Println(title)
|
||||
fmt.Println(strings.Repeat("-", len(title)))
|
||||
}
|
||||
|
||||
// PrintSuccess prints a success message
|
||||
func PrintSuccess(message string) {
|
||||
fmt.Printf("✓ %s\n", message)
|
||||
}
|
||||
|
||||
// PrintError prints an error message
|
||||
func PrintError(message string) {
|
||||
fmt.Fprintf(os.Stderr, "✗ %s\n", message)
|
||||
}
|
||||
|
||||
// PrintInfo prints an informational message
|
||||
func PrintInfo(message string) {
|
||||
fmt.Printf("ℹ %s\n", message)
|
||||
}
|
||||
|
||||
// Confirm asks for confirmation before proceeding
|
||||
func Confirm(prompt string) bool {
|
||||
return PromptBool(prompt, false)
|
||||
}
|
||||
Reference in New Issue
Block a user