package config import ( "bufio" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" "time" "golang.org/x/term" ) const DefaultSyncInterval = "5m" // Config holds runtime settings. JSON field names are stable for operators editing by hand. type Config struct { Endpoint string `json:"endpoint"` APIKey string `json:"api_key"` PlayersDBPath string `json:"players_db_path"` SyncInterval string `json:"sync_interval"` } // DefaultPath returns config.json next to the executable. func DefaultPath() (string, error) { exe, err := os.Executable() if err != nil { return "", err } exe, err = filepath.EvalSymlinks(exe) if err != nil { return "", err } return filepath.Join(filepath.Dir(exe), "config.json"), nil } func (c *Config) Validate() error { if strings.TrimSpace(c.Endpoint) == "" { return errors.New("endpoint is required") } if strings.TrimSpace(c.APIKey) == "" { return errors.New("api_key is required") } if strings.TrimSpace(c.PlayersDBPath) == "" { return errors.New("players_db_path is required") } interval := strings.TrimSpace(c.SyncInterval) if interval == "" { interval = DefaultSyncInterval } if _, err := time.ParseDuration(interval); err != nil { return fmt.Errorf("sync_interval: %w", err) } return nil } func (c *Config) IntervalDuration() (time.Duration, error) { s := strings.TrimSpace(c.SyncInterval) if s == "" { s = DefaultSyncInterval } return time.ParseDuration(s) } func Load(path string) (*Config, error) { b, err := os.ReadFile(path) if err != nil { return nil, err } var c Config if err := json.Unmarshal(b, &c); err != nil { return nil, err } return &c, nil } func Save(path string, c *Config) error { b, err := json.MarshalIndent(c, "", " ") if err != nil { return err } return os.WriteFile(path, b, 0600) } // RunWizard interactively collects settings and writes config to path. Requires a TTY on stdin. func RunWizard(path string) error { if !term.IsTerminal(int(os.Stdin.Fd())) { return errors.New("stdin is not a terminal: run this program from a console to create config.json, or create the file by hand") } fmt.Fprintln(os.Stderr, "tx-sync: first-time setup — values are saved to:", path) fmt.Fprintln(os.Stderr) endpoint := promptLine("HTTPS endpoint URL (POST, full URL): ") apiKey, err := promptPassword("API key (X-API-Key, hidden): ") if err != nil { return err } dbPath := promptLine("Path to playersDB.json: ") interval := promptLine(fmt.Sprintf("Sync interval [%s]: ", DefaultSyncInterval)) if strings.TrimSpace(interval) == "" { interval = DefaultSyncInterval } cfg := Config{ Endpoint: strings.TrimSpace(endpoint), APIKey: strings.TrimSpace(apiKey), PlayersDBPath: strings.TrimSpace(dbPath), SyncInterval: strings.TrimSpace(interval), } if err := cfg.Validate(); err != nil { return err } if err := Save(path, &cfg); err != nil { return err } fmt.Fprintln(os.Stderr) fmt.Fprintln(os.Stderr, "Configuration saved. Run tx-sync again to start syncing (immediate upload, then on the timer).") return nil } func promptLine(label string) string { fmt.Fprint(os.Stderr, label) s := bufio.NewScanner(os.Stdin) if !s.Scan() { return "" } return s.Text() } func promptPassword(label string) (string, error) { fmt.Fprint(os.Stderr, label) b, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Fprintln(os.Stderr) if err != nil { return "", err } return string(b), nil }