Files
GitManager/internal/config/config.go
T
TBNilles e30c3b632a Move domain config from .env to a private SQLite store
Forges (multi-host) + tokens, project directories, and git identity now live in a private SQLite config store (internal/store, modernc.org/sqlite) on a /data named volume that is not bind-mounted or exposed, so credentials aren't reachable outside the container. New Settings page (/settings) + <settings-panel> with /api/config CRUD. Scanner reads roots fresh from the store each cycle; service resolves forges per-repo from the store and reapplies per-forge git auth on change. First run seeds the store from .env. Overturns the old no-datastore/.env-config laws (AGENT.md updated). Verified live end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-22 05:31:28 -04:00

120 lines
3.9 KiB
Go

// 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
// DBPath is the SQLite config store (forges, project dirs, git identity).
// It lives on a private volume, not accessible outside the container (§1.3).
DBPath string
RepoRoots []string // SEED ONLY: roots to scan, used to seed the store on first run
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
GitUserName string // commit identity for git actions run by the app
GitUserEmail string // commit identity for git actions run by the app
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"),
DBPath: env("GITMANAGER_DB", "/data/gitmanager.db"),
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", ""),
GitUserName: env("GIT_USER_NAME", ""),
GitUserEmail: env("GIT_USER_EMAIL", ""),
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
}