obm/internal/config/dotenv_writer.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

166 lines
4.1 KiB
Go

package config
import (
"bufio"
"fmt"
"os"
"sort"
"strings"
)
// WriteDotEnv generates a .env file from a Config.
// The output includes:
// - Header comment with usage instructions
// - Variables organized by group
// - Comments with descriptions
// - Values formatted appropriately (quoted if needed)
// - Sensitive values marked with YOUR_..._HERE placeholders if empty
func WriteDotEnv(cfg *Config, path string) error {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating .env file %s: %w", path, err)
}
defer f.Close()
w := bufio.NewWriter(f)
defer w.Flush()
// Write header
header := `# OpenBoatmobile Environment Variables
# Copy to .env and fill in your values, then source it:
# source .env && terraform init && terraform plan
#
# Variables prefixed with TF_VAR_ are automatically picked up by Terraform.
# This file is gitignored — never commit secrets!
`
fmt.Fprint(w, header)
// Write variables by group
groups := VarsByGroup()
groupOrder := []VarGroup{
GroupProvider, GroupProviderHetzner, GroupProviderDO,
GroupServer, GroupSSH,
GroupAPIKeys, GroupModel,
GroupDiscord, GroupTailscale,
GroupHermes, GroupOpenClaw,
GroupSecurity, GroupProject,
}
writtenVars := make(map[string]bool)
for _, group := range groupOrder {
vars := groups[group]
if len(vars) == 0 {
continue
}
// Write group header
fmt.Fprintf(w, "\n# ===%s===\n", strings.Repeat("=", 73-len(string(group))-len("===")))
fmt.Fprintf(w, "# %s\n", group)
fmt.Fprintf(w, "# %s\n", strings.Repeat("-", len(group)))
fmt.Fprintln(w)
for _, v := range vars {
if writtenVars[v.Name] {
continue // Skip duplicates (variables can appear in multiple groups conceptually)
}
writtenVars[v.Name] = true
writeDotEnvVar(w, v, cfg)
}
}
// Write any additional variables not in schema (custom user vars)
customVars := []string{}
for name := range cfg.Variables {
if !writtenVars[name] {
customVars = append(customVars, name)
}
}
if len(customVars) > 0 {
fmt.Fprint(w, "\n# === Custom Variables ===\n")
sort.Strings(customVars)
for _, name := range customVars {
val := cfg.Variables[name]
fmt.Fprintf(w, "TF_VAR_%s=%s\n", name, formatDotEnvValue(val))
}
}
return nil
}
// writeDotEnvVar writes a single variable to the .env file.
func writeDotEnvVar(w *bufio.Writer, v VarDef, cfg *Config) {
val, ok := cfg.GetValue(v.Name)
if !ok {
val = v.Default
}
// Write description comment
if v.Description != "" {
fmt.Fprintf(w, "# %s\n", v.Description)
}
// Mark required/sensitive in comment
requirements := []string{}
if v.Required {
requirements = append(requirements, "REQUIRED")
}
if v.Sensitive {
requirements = append(requirements, "secret")
}
if len(requirements) > 0 {
fmt.Fprintf(w, "# %s\n", strings.Join(requirements, ", "))
}
// Special handling for empty required/sensitive values
if val == "" && (v.Required || v.Sensitive) {
val = fmt.Sprintf("YOUR_%s_HERE", strings.ToUpper(v.Name))
}
// Write variable
fmt.Fprintf(w, "TF_VAR_%s=%s", v.Name, formatDotEnvValue(val))
// Add inline comment hint if available
if v.EnvComment != "" {
fmt.Fprintf(w, " # %s", v.EnvComment)
}
fmt.Fprintln(w)
fmt.Fprintln(w) // Blank line after each var
}
// formatDotEnvValue formats a value for .env output.
// Quoting rules:
// - Empty string -> ""
// - Values with spaces, #, or special chars -> quoted
// - Lists -> '[\"item1\", \"item2\"]'
func formatDotEnvValue(val string) string {
if val == "" {
return "\"\""
}
// Check if already a list format
if strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]") {
return val
}
// Check if needs quoting
needsQuote := strings.ContainsAny(val, " \t#\"'`$") ||
strings.Contains(val, "\n")
if needsQuote {
// Use double quotes, escape inner double quotes
escaped := strings.ReplaceAll(val, "\"", "\\\"")
return fmt.Sprintf("\"%s\"", escaped)
}
return val
}
// FormatDotEnvVar formats a single variable for display (not file output).
func FormatDotEnvVar(name, value string) string {
if value == "" {
return fmt.Sprintf("TF_VAR_%s=\"\"", name)
}
return fmt.Sprintf("TF_VAR_%s=%s", name, formatDotEnvValue(value))
}