- Interactive deploy command with 8-step walkthrough: framework → provider → token → SSH → server → inference → tailscale → discord - .env file generation from walkthrough config - DeploymentConfig struct with framework-aware defaults - Inference API client with validation for Venice, OpenRouter, OpenAI, Anthropic - Hetzner Cloud provider: token validation, SSH key listing - DotEnv parser/writer with schema validation - Destroy command with confirmation prompt - Validation subcommand for checking existing .env files - All tests passing, go vet clean
216 lines
5.3 KiB
Go
216 lines
5.3 KiB
Go
// Package prompt handles interactive user prompts and confirmations.
|
||
package prompt
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// ANSI color codes
|
||
const (
|
||
reset = "\033[0m"
|
||
bold = "\033[1m"
|
||
red = "\033[31m"
|
||
green = "\033[32m"
|
||
yellow = "\033[33m"
|
||
cyan = "\033[36m"
|
||
gray = "\033[90m"
|
||
)
|
||
|
||
// StepHeader prints a numbered step header.
|
||
func StepHeader(step int, title string) {
|
||
fmt.Printf("\n%sStep %d: %s%s\n", bold+cyan, step, title, reset)
|
||
}
|
||
|
||
// Select displays a numbered menu and returns the 1-based index of the selection.
|
||
// Returns the selected index (1-based) or error.
|
||
func Select(prompt string, options []string) (int, error) {
|
||
fmt.Printf("\n%s%s%s\n", bold, prompt, reset)
|
||
for i, opt := range options {
|
||
fmt.Printf(" %s[%d]%s %s\n", cyan, i+1, reset, opt)
|
||
}
|
||
fmt.Printf("\n> ")
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
input, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
return 0, fmt.Errorf("reading input: %w", err)
|
||
}
|
||
input = strings.TrimSpace(input)
|
||
|
||
idx, err := strconv.Atoi(input)
|
||
if err != nil || idx < 1 || idx > len(options) {
|
||
return 0, fmt.Errorf("invalid selection: %s (choose 1-%d)", input, len(options))
|
||
}
|
||
return idx, nil
|
||
}
|
||
|
||
// SelectWithDefault displays a numbered menu with a default selection.
|
||
// Pressing Enter selects the default.
|
||
func SelectWithDefault(prompt string, options []string, defaultIdx int) (int, error) {
|
||
fmt.Printf("\n%s%s%s\n", bold, prompt, reset)
|
||
for i, opt := range options {
|
||
marker := " "
|
||
if i+1 == defaultIdx {
|
||
marker = "*"
|
||
}
|
||
fmt.Printf(" %s[%d]%s %s %s\n", cyan, i+1, reset, opt, grayMarker(marker))
|
||
}
|
||
fmt.Printf("\n> [%d] ", defaultIdx)
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
input, err := reader.ReadString('\n')
|
||
if err != nil {
|
||
return 0, fmt.Errorf("reading input: %w", err)
|
||
}
|
||
input = strings.TrimSpace(input)
|
||
|
||
if input == "" {
|
||
return defaultIdx, nil
|
||
}
|
||
|
||
idx, err := strconv.Atoi(input)
|
||
if err != nil || idx < 1 || idx > len(options) {
|
||
return 0, fmt.Errorf("invalid selection: %s (choose 1-%d)", input, len(options))
|
||
}
|
||
return idx, nil
|
||
}
|
||
|
||
// Confirm asks a yes/no question. Default is no unless defaultYes is true.
|
||
func Confirm(message string, defaultYes bool) bool {
|
||
if defaultYes {
|
||
fmt.Printf("%s [Y/n]: ", message)
|
||
} else {
|
||
fmt.Printf("%s [y/N]: ", message)
|
||
}
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
input, _ := reader.ReadString('\n')
|
||
input = strings.TrimSpace(strings.ToLower(input))
|
||
|
||
if input == "" {
|
||
return defaultYes
|
||
}
|
||
return input == "y" || input == "yes"
|
||
}
|
||
|
||
// Input asks for free text with an optional default value.
|
||
func Input(label string, defaultValue string) string {
|
||
if defaultValue != "" {
|
||
fmt.Printf("%s [%s]: ", label, defaultValue)
|
||
} else {
|
||
fmt.Printf("%s: ", label)
|
||
}
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
input, _ := reader.ReadString('\n')
|
||
input = strings.TrimSpace(input)
|
||
|
||
if input == "" {
|
||
return defaultValue
|
||
}
|
||
return input
|
||
}
|
||
|
||
// Password asks for sensitive input. Characters are replaced with asterisks on display.
|
||
func Password(label string) string {
|
||
fmt.Printf("%s: ", label)
|
||
|
||
reader := bufio.NewReader(os.Stdin)
|
||
input, _ := reader.ReadString('\n')
|
||
input = strings.TrimSpace(input)
|
||
|
||
// Print asterisks to replace the entered text
|
||
mask := strings.Repeat("*", len(input))
|
||
fmt.Printf("\033[A%s: %s\n", label, mask)
|
||
|
||
return input
|
||
}
|
||
|
||
// ValidateFunc is a function that validates input. Returns empty string if valid,
|
||
// or an error message if invalid.
|
||
type ValidateFunc func(string) string
|
||
|
||
// InputValidated asks for input with validation. Retries until valid.
|
||
func InputValidated(label string, defaultValue string, validate ValidateFunc) string {
|
||
for {
|
||
value := Input(label, defaultValue)
|
||
if validate == nil {
|
||
return value
|
||
}
|
||
if errMsg := validate(value); errMsg != "" {
|
||
Error(errMsg)
|
||
continue
|
||
}
|
||
return value
|
||
}
|
||
}
|
||
|
||
// PasswordValidated asks for sensitive input with validation. Retries until valid.
|
||
func PasswordValidated(label string, validate ValidateFunc) string {
|
||
for {
|
||
value := Password(label)
|
||
if validate == nil {
|
||
return value
|
||
}
|
||
if errMsg := validate(value); errMsg != "" {
|
||
Error(errMsg)
|
||
continue
|
||
}
|
||
return value
|
||
}
|
||
}
|
||
|
||
// Success prints a green checkmark message.
|
||
func Success(message string) {
|
||
fmt.Printf(" %s✓%s %s\n", green, reset, message)
|
||
}
|
||
|
||
// Error prints a red X message.
|
||
func Error(message string) {
|
||
fmt.Printf(" %s✗%s %s\n", red, reset, message)
|
||
}
|
||
|
||
// Warn prints a yellow warning message.
|
||
func Warn(message string) {
|
||
fmt.Printf(" %s⚠%s %s\n", yellow, reset, message)
|
||
}
|
||
|
||
// Info prints a gray info message.
|
||
func Info(message string) {
|
||
fmt.Printf(" %sℹ%s %s\n", gray, reset, message)
|
||
}
|
||
|
||
// Header prints a bold header.
|
||
func Header(message string) {
|
||
fmt.Printf("\n%s%s%s\n", bold, message, reset)
|
||
}
|
||
|
||
// Divider prints a horizontal divider.
|
||
func Divider() {
|
||
fmt.Printf("%s──────────────────────────────────%s\n", gray, reset)
|
||
}
|
||
|
||
// SummaryLine prints a key-value pair in summary format.
|
||
func SummaryLine(key, value string) {
|
||
fmt.Printf(" %-20s %s\n", key+":", value)
|
||
}
|
||
|
||
// MaskValue masks a sensitive value, showing only the last 4 characters.
|
||
// Values of 8 characters or shorter are fully masked.
|
||
func MaskValue(v string) string {
|
||
if len(v) <= 8 {
|
||
return "****"
|
||
}
|
||
return "****" + v[len(v)-4:]
|
||
}
|
||
|
||
func grayMarker(marker string) string {
|
||
if marker == " " {
|
||
return ""
|
||
}
|
||
return gray + marker + reset
|
||
}
|