package config import ( "fmt" "log" "os" "gopkg.in/yaml.v3" ) type Config struct { Tools map[string]yaml.Node `yaml:"tools" json:"tools"` Profiles map[string]Profile `yaml:"profiles" json:"profiles"` } type Profile struct { Notes string `yaml:"notes,omitempty" json:"notes,omitempty"` Tools map[string]yaml.Node `yaml:"tools" json:"tools"` Enabled []string `yaml:"enabled" json:"enabled"` Disabled []string `yaml:"disabled" json:"disabled"` } func (c *Config) DecodeEffective(toolName, profileName string, dst any) error { if node, ok := c.Tools[toolName]; ok { if err := node.Decode(dst); err != nil { return fmt.Errorf("config: decoding global config for tool %q: %w", toolName, err) } } if profileName != "" { // Builtin profiles have their overrides defined in Go, not in YAML. if _, isBuiltin := BuiltinProfiles[profileName]; isBuiltin { return ApplyBuiltinToolOverride(profileName, toolName, dst) } p, ok := c.Profiles[profileName] if !ok { return fmt.Errorf("config: unknown profile %q", profileName) } if node, ok := p.Tools[toolName]; ok { if err := node.Decode(dst); err != nil { return fmt.Errorf("config: decoding profile %q override for tool %q: %w", profileName, toolName, err) } } } return nil } func (c *Config) ActiveTools(profileName string, allToolNames []string) ([]string, error) { if profileName == "" { return allToolNames, nil } if builtin, ok := BuiltinProfiles[profileName]; ok { return ActiveToolsForProfile(builtin.Profile, allToolNames), nil } p, ok := c.Profiles[profileName] if !ok { return nil, fmt.Errorf("config: unknown profile %q", profileName) } return ActiveToolsForProfile(p, allToolNames), nil } // IsReadonly reports whether the config file at path cannot be written to. // Returns false if the file does not exist (it can still be created). func IsReadonly(path string) bool { f, err := os.OpenFile(path, os.O_WRONLY, 0) if err != nil { return os.IsPermission(err) } f.Close() return false } func Load(path string) (*Config, error) { f, err := os.Open(path) if os.IsNotExist(err) { log.Printf("config: %q not found, starting with empty config", path) return Default(), nil } if err != nil { return nil, fmt.Errorf("config: open %q: %w", path, err) } defer func() { _ = f.Close() }() var cfg Config dec := yaml.NewDecoder(f) dec.KnownFields(true) if err := dec.Decode(&cfg); err != nil { return nil, fmt.Errorf("config: decode: %w", err) } return &cfg, nil } func Default() *Config { return &Config{ Tools: make(map[string]yaml.Node), Profiles: make(map[string]Profile), } } func Save(path string, cfg *Config) error { f, err := os.Create(path) if err != nil { return fmt.Errorf("config: create %q: %w", path, err) } defer func() { _ = f.Close() }() enc := yaml.NewEncoder(f) enc.SetIndent(2) if err := enc.Encode(cfg); err != nil { return fmt.Errorf("config: encode: %w", err) } return nil } // MergeNodePatch merges patch key-values into an existing yaml.Node (mapping). // If existing is a zero value, it starts from an empty mapping. func MergeNodePatch(existing yaml.Node, patch map[string]any) (yaml.Node, error) { var m map[string]any if existing.Kind != 0 { if err := existing.Decode(&m); err != nil { return yaml.Node{}, fmt.Errorf("config: decode node: %w", err) } } if m == nil { m = make(map[string]any) } for k, v := range patch { m[k] = v } b, err := yaml.Marshal(m) if err != nil { return yaml.Node{}, fmt.Errorf("config: marshal: %w", err) } var doc yaml.Node if err := yaml.Unmarshal(b, &doc); err != nil { return yaml.Node{}, fmt.Errorf("config: unmarshal: %w", err) } if doc.Kind == yaml.DocumentNode && len(doc.Content) == 1 { return *doc.Content[0], nil } return doc, nil }