package config import ( "errors" "fmt" "os" "path/filepath" "gopkg.in/yaml.v3" ) // Config is the on-disk settings file. Add fields here as the tool grows. type Config struct { // ReposRoots are parent directories scanned (one level deep) for git repos. ReposRoots []string `yaml:"repos_roots"` // DevDir is the `dev` repo: holds commands/awsmfa, docker-compose.yml, bin/*.sh. DevDir string `yaml:"dev_dir"` // LegacyDir is the `legacy` repo: holds rundev.sh for prod-data console tools. LegacyDir string `yaml:"legacy_dir"` } // ErrNotSet means the config file does not exist yet. var ErrNotSet = errors.New("config not set") // Path returns the config file location: $XDG_CONFIG_HOME/mydev/config.yaml, // falling back to ~/.config/mydev/config.yaml on all platforms. func Path() (string, error) { base := os.Getenv("XDG_CONFIG_HOME") if base == "" { home, err := os.UserHomeDir() if err != nil { return "", err } base = filepath.Join(home, ".config") } return filepath.Join(base, "mydev", "config.yaml"), nil } // Load reads and parses the config file. Returns ErrNotSet (wrapped) if the // file does not exist — callers should surface that as a clear "run config init". func Load() (*Config, error) { path, err := Path() if err != nil { return nil, err } data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, fmt.Errorf("%w: no config at %s — run 'mydev config init'", ErrNotSet, path) } return nil, err } var c Config if err := yaml.Unmarshal(data, &c); err != nil { return nil, fmt.Errorf("parse %s: %w", path, err) } return &c, nil } // --- validated accessors: each errors clearly when its setting is missing --- func (c *Config) requireDev() (string, error) { if c.DevDir == "" { return "", errors.New("dev_dir not set in config — add it and retry") } return c.DevDir, nil } func (c *Config) requireLegacy() (string, error) { if c.LegacyDir == "" { return "", errors.New("legacy_dir not set in config — add it and retry") } return c.LegacyDir, nil } // AWSMFA returns the path to the awsmfa command inside dev_dir. func (c *Config) AWSMFA() (string, error) { dev, err := c.requireDev() if err != nil { return "", err } return filepath.Join(dev, "commands", "awsmfa"), nil } // ComposeFile returns the dev docker-compose.yml path. func (c *Config) ComposeFile() (string, error) { dev, err := c.requireDev() if err != nil { return "", err } return filepath.Join(dev, "docker-compose.yml"), nil } // DevBin returns the path to a script under dev_dir/bin. func (c *Config) DevBin(script string) (string, error) { dev, err := c.requireDev() if err != nil { return "", err } return filepath.Join(dev, "bin", script), nil } // LegacyRunDev returns the legacy rundev.sh path (runs cmds in the container). func (c *Config) LegacyRunDev() (string, error) { legacy, err := c.requireLegacy() if err != nil { return "", err } return filepath.Join(legacy, "rundev.sh"), nil } const sample = `# MyDev config # Parent directories scanned one level deep for git repos. repos_roots: - %[1]s/code # The 'dev' repo — provides commands/awsmfa, docker-compose.yml, bin/*.sh. dev_dir: %[1]s/code/dev # The 'legacy' repo — provides rundev.sh for prod-data console tools. legacy_dir: %[1]s/code/legacy ` // Init writes a starter config file if none exists. Returns the path written. // Refuses to overwrite an existing file. func Init() (string, error) { path, err := Path() if err != nil { return "", err } if _, err := os.Stat(path); err == nil { return path, fmt.Errorf("config already exists at %s", path) } if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return "", err } home, _ := os.UserHomeDir() if home == "" { home = "/Users/you" } if err := os.WriteFile(path, []byte(fmt.Sprintf(sample, home)), 0o644); err != nil { return "", err } return path, nil }