mydev/internal/config/config.go
Victor Abrell 7140397a13 Initial commit: MyDev dev-environment TUI
CLI + live dashboard wrapping aws/docker/git/awsmfa for local dev:
AWS MFA auth status + expiry, dev compose stack control, container
drift vs the dev repo, git repo status, prod-data and DB-access tools.
Config-driven (~/.config/mydev/config.yaml). Dashboard runs commands
via a command palette with captured/interactive modes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:31:58 +02:00

147 lines
3.8 KiB
Go

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
}