// Package config loads GitManager's runtime configuration from the environment // (and an optional .env file). See AGENT.md §1.5 — every setting is env-driven; // nothing is hardcoded. package config import ( "os" "strconv" "strings" "time" "github.com/joho/godotenv" ) // Config is the typed application configuration. type Config struct { ListenAddr string // address the plain HTTP server binds to // TLS: when HTTPSAddr and both cert/key files are set, an HTTPS listener is // started in addition to the HTTP one. Claude Desktop's MCP connector only // accepts https:// URLs, so /mcp must be reachable over TLS (AGENT.md §8.1). HTTPSAddr string // address the HTTPS server binds to ("" disables TLS) TLSCertFile string // PEM cert (e.g. an mkcert leaf trusted by the OS store) TLSKeyFile string // PEM private key RepoRoots []string // roots to scan for git repositories GitBin string // path to the git binary ScanInterval time.Duration // scanner refresh interval ScanMaxDepth int // max discovery depth under each root ScanIgnore []string // directory names to skip during discovery ScanFetchEnabled bool // allow the scanner to run `git fetch` Dev bool // readable console logging vs structured JSON LogFile string // optional file to also append logs to GiteaURL string // Gitea/Forgejo base URL (e.g. https://git.nilles.net) GiteaToken string // Gitea token (read + PR write + branch delete) — §8.4 GitHubToken string // optional forge token (later provider) GitLabToken string // optional forge token (later provider) } // Load reads .env (if present) then the environment, applying defaults. // A missing .env is not an error — the environment may be set another way. func Load() (Config, error) { _ = godotenv.Load() c := Config{ ListenAddr: env("LISTEN_ADDR", "127.0.0.1:8080"), HTTPSAddr: env("HTTPS_ADDR", ""), TLSCertFile: env("TLS_CERT_FILE", ""), TLSKeyFile: env("TLS_KEY_FILE", ""), RepoRoots: splitList(env("GIT_REPO_ROOTS", "")), GitBin: env("GIT_BIN", "git"), ScanMaxDepth: envInt("SCAN_MAX_DEPTH", 4), ScanIgnore: splitList(env("SCAN_IGNORE", "node_modules,vendor,.cache")), ScanFetchEnabled: envBool("SCAN_FETCH_ENABLED", false), Dev: strings.EqualFold(env("APP_ENV", "dev"), "dev"), LogFile: env("LOG_FILE", ""), GiteaURL: env("GITEA_URL", ""), GiteaToken: env("GITEA_TOKEN", ""), GitHubToken: env("GITHUB_TOKEN", ""), GitLabToken: env("GITLAB_TOKEN", ""), } interval, err := time.ParseDuration(env("SCAN_INTERVAL", "30s")) if err != nil { return Config{}, err } c.ScanInterval = interval return c, nil } func env(key, def string) string { if v, ok := os.LookupEnv(key); ok && v != "" { return v } return def } func envInt(key string, def int) int { if v, ok := os.LookupEnv(key); ok { if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil { return n } } return def } func envBool(key string, def bool) bool { if v, ok := os.LookupEnv(key); ok { if b, err := strconv.ParseBool(strings.TrimSpace(v)); err == nil { return b } } return def } // splitList splits a comma-separated value, trimming spaces and dropping empties. func splitList(v string) []string { var out []string for _, part := range strings.Split(v, ",") { if p := strings.TrimSpace(part); p != "" { out = append(out, p) } } return out }