- 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
166 lines
4.1 KiB
Go
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))
|
|
}
|