package config import ( "errors" "fmt" "os" "path/filepath" "sort" "strings" ) // LocalFilename is the per-workspace override file name. const LocalFilename = "config.local" // WriteLocalKeys upserts key=value pairs into /config.local, // preserving comments, blank lines, unknown keys, and line order. An // existing non-comment line whose key matches is replaced in place; a // key not yet present is appended (appended keys in sorted order for a // deterministic file). The workspace directory must already exist — // settings are an initialised-workspace operation, like gc and new. // // It only edits config.local inside the gitignored workspace; it never // touches the tracked tree. func WriteLocalKeys(cfg *Config, kv map[string]string) error { if cfg == nil { return errors.New("WriteLocalKeys: nil config") } if len(kv) == 0 { return nil } info, err := os.Stat(cfg.Workspace) if err != nil || !info.IsDir() { return fmt.Errorf("workspace %s is not initialised", cfg.Workspace) } return upsertKeys(filepath.Join(cfg.Workspace, LocalFilename), kv) } // WriteGlobalKeys upserts key=value pairs into the user-global // config.local (GlobalConfigDir()/config.local), creating the global // directory if absent. Same upsert semantics as WriteLocalKeys. This is // the one writer that touches a file outside any repo, by design — it // backs `eeco config set --global`, the cross-project settings layer. func WriteGlobalKeys(kv map[string]string) error { if len(kv) == 0 { return nil } dir := GlobalConfigDir() if dir == "" { return errors.New("cannot resolve a global config directory (set EECO_CONFIG_HOME or HOME)") } if err := os.MkdirAll(dir, 0o755); err != nil { return fmt.Errorf("create global config dir: %w", err) } return upsertKeys(filepath.Join(dir, LocalFilename), kv) } // upsertKeys writes key=value pairs into the config.local-format file at // path, preserving comments, blank lines, unknown keys, and line order. // An existing non-comment line whose key matches is replaced in place; a // key not yet present is appended (appended keys in sorted order for a // deterministic file). func upsertKeys(path string, kv map[string]string) error { existing, err := os.ReadFile(path) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("read %s: %w", LocalFilename, err) } written := map[string]bool{} var out []string if len(existing) > 0 { for _, raw := range strings.Split(strings.TrimRight(string(existing), "\n"), "\n") { trimmed := strings.TrimSpace(raw) if trimmed == "" || strings.HasPrefix(trimmed, "#") { out = append(out, raw) continue } k, _, ok := strings.Cut(trimmed, "=") if !ok { out = append(out, raw) continue } key := strings.TrimSpace(k) if v, replace := kv[key]; replace && !written[key] { out = append(out, key+"="+v) written[key] = true continue } out = append(out, raw) } } var fresh []string for k := range kv { if !written[k] { fresh = append(fresh, k) } } sort.Strings(fresh) for _, k := range fresh { out = append(out, k+"="+kv[k]) } return os.WriteFile(path, []byte(strings.Join(out, "\n")+"\n"), 0o644) }