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>
147 lines
3.8 KiB
Go
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
|
|
}
|