// Package config handles configuration management for the Preloop CLI.
package config
import (
"fmt"
"os"
"path/filepath"
"github.com/spf13/viper"
)
// Environment variable names.
const (
EnvToken = "PRELOOP_TOKEN"
EnvURL = "PRELOOP_URL"
)
const (
// ConfigDir is the directory name for preloop config.
ConfigDir = ".preloop"
// ConfigFile is the config file name.
ConfigFile = "config.yaml"
// DefaultAPIURL is the default API endpoint.
DefaultAPIURL = "https://preloop.ai"
)
// Config represents the CLI configuration.
type Config struct {
AccessToken string `mapstructure:"access_token"`
RefreshToken string `mapstructure:"refresh_token"`
APIURL string `mapstructure:"api_url"`
}
// configPath returns the full path to the config file.
func configPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}
return filepath.Join(home, ConfigDir, ConfigFile), nil
}
// ConfigDir returns the path to the config directory.
func GetConfigDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to get home directory: %w", err)
}
return filepath.Join(home, ConfigDir), nil
}
// ensureConfigDir creates the config directory if it doesn't exist.
func ensureConfigDir() error {
dir, err := GetConfigDir()
if err != nil {
return err
}
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("failed to create config directory: %w", err)
}
return nil
}
// Load reads the configuration from ~/.preloop/config.yaml.
func Load() (*Config, error) {
cfgPath, err := configPath()
if err != nil {
return nil, err
}
v := viper.New()
v.SetConfigFile(cfgPath)
v.SetConfigType("yaml")
// Set defaults
v.SetDefault("api_url", DefaultAPIURL)
// Read config file (ignore error if file doesn't exist)
if err := v.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
// Only return error if it's not a "file not found" error
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to read config: %w", err)
}
}
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &cfg, nil
}
// Save writes the configuration to ~/.preloop/config.yaml.
func Save(cfg *Config) error {
if err := ensureConfigDir(); err != nil {
return err
}
cfgPath, err := configPath()
if err != nil {
return err
}
v := viper.New()
v.SetConfigFile(cfgPath)
v.SetConfigType("yaml")
v.Set("access_token", cfg.AccessToken)
v.Set("refresh_token", cfg.RefreshToken)
v.Set("api_url", cfg.APIURL)
if err := v.WriteConfig(); err != nil {
// If config file doesn't exist, create it
if os.IsNotExist(err) {
return v.SafeWriteConfig()
}
return fmt.Errorf("failed to write config: %w", err)
}
return nil
}
// Clear removes all authentication tokens from the config.
func Clear() error {
cfg, err := Load()
if err != nil {
return err
}
cfg.AccessToken = ""
cfg.RefreshToken = ""
return Save(cfg)
}
// SetTokens updates the access and refresh tokens in the config.
func SetTokens(accessToken, refreshToken string) error {
cfg, err := Load()
if err != nil {
return err
}
cfg.AccessToken = accessToken
cfg.RefreshToken = refreshToken
return Save(cfg)
}
// SetAPIURL updates the API URL in the config.
func SetAPIURL(apiURL string) error {
cfg, err := Load()
if err != nil {
return err
}
cfg.APIURL = apiURL
return Save(cfg)
}
// IsAuthenticated returns true if an access token is configured
// (from config file, env var, or CLI flag).
func IsAuthenticated() bool {
cfg, err := Load()
if err != nil {
return false
}
return cfg.AccessToken != ""
}
// Resolve returns a Config with values resolved in priority order:
// CLI flags (overrides) > environment variables > config file > defaults.
func Resolve(tokenOverride, urlOverride string) (*Config, error) {
cfg, err := Load()
if err != nil {
return nil, err
}
// Environment variables override config file
if v := os.Getenv(EnvToken); v != "" {
cfg.AccessToken = v
}
if v := os.Getenv(EnvURL); v != "" {
cfg.APIURL = v
}
// CLI flags override everything
if tokenOverride != "" {
cfg.AccessToken = tokenOverride
}
if urlOverride != "" {
cfg.APIURL = urlOverride
}
// Ensure default
if cfg.APIURL == "" {
cfg.APIURL = DefaultAPIURL
}
return cfg, nil
}