obm/internal/prompt/prompt.go
MermaidMan 33d9a2cb2e deploy walkthrough, API validation, inference client, Hetzner provider
- 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
2026-05-22 15:29:27 +00:00

216 lines
5.3 KiB
Go
Raw 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 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
}