Files
joakim 140d9f7f25 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
2026-01-06 21:49:13 +01:00

145 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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.
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)
}