// Package config — загрузка конфигурации Ratatoskr-go. // // Источники (каждый следующий имеет приоритет над предыдущим): // 1. Файл config.yaml (с подстановкой ${VAR:-default}) // 2. Переменные окружения (env-теги в структуре) // // Порядок: Load("config.yaml") → YAML → os.Expand → env override → Validate. package config import ( "errors" "fmt" "time" ) // Duration — time.Duration с YAML-сериализацией (строка "5s", "10m"). type Duration time.Duration func (d Duration) Duration() time.Duration { return time.Duration(d) } func (d Duration) String() string { return time.Duration(d).String() } func (d *Duration) UnmarshalYAML(unmarshal func(any) error) error { var s string if err := unmarshal(&s); err != nil { return fmt.Errorf("%w: %v", ErrInvalidFormat, err) } td, err := time.ParseDuration(s) if err != nil { return fmt.Errorf("%w: parse duration %q: %v", ErrInvalidFormat, s, err) } *d = Duration(td) return nil } // Config — полная конфигурация бинаря. type Config struct { Telegram TelegramCfg `yaml:"telegram"` OpenCode OpenCodeCfg `yaml:"opencode"` Git GitCfg `yaml:"git"` Chat ChatCfg `yaml:"chat"` Paths PathsCfg `yaml:"paths"` Update UpdateCfg `yaml:"update"` } // GitCfg — источник репозиториев (для git clone). type GitCfg struct { BaseURL string `yaml:"base_url" env:"GIT_BASE_URL"` Token string `yaml:"token" env:"GIT_TOKEN"` } // UpdateCfg — автообновление. // Enabled=false отключает и авто-проверку, и /update. // BaseURL — хост Gitea для обновления (обязателен: без него update отключён, // фоллбэк на git.base_url НЕ используется). // Token — токен read:package. Приоритет: вшитый -X main.updateToken > этот. // Package — имя пакета-хранилища (по умолчанию ratatoskr). Владелец пакета — // жёсткая константа (packageOwner в app). // CheckInterval — как часто авто-проверять наличие обновления // (по умолчанию 24h). Авто-режим ТОЛЬКО уведомляет; замена — по /update. type UpdateCfg struct { Enabled bool `yaml:"enabled" default:"false"` BaseURL string `yaml:"base_url" env:"UPDATE_BASE_URL"` Token string `yaml:"token" env:"UPDATE_TOKEN"` Package string `yaml:"package" default:"ratatoskr"` CheckInterval Duration `yaml:"check_interval" default:"24h"` } type TelegramCfg struct { Token string `yaml:"token" env:"TG_TOKEN"` ChatID string `yaml:"chat_id" env:"TG_CHAT_ID"` } type OpenCodeCfg struct { Bin string `yaml:"bin" default:"opencode"` DBPath string `yaml:"db_path" default:""` Config string `yaml:"config" env:"OPENCODE_CONFIG"` ConfigDir string `yaml:"config_dir" env:"OPENCODE_CONFIG_DIR"` HardTimeout Duration `yaml:"hard_timeout" default:"20m"` IdleTimeout Duration `yaml:"idle_timeout" default:"2m"` PollMs Duration `yaml:"poll_ms" default:"2s"` } type ChatCfg struct { PollInterval Duration `yaml:"poll_interval" default:"30s"` } type PathsCfg struct { Worktree string `yaml:"worktree" default:"./worktrees"` DB string `yaml:"db" default:"./ratatoskr.db" env:"RATATOSKR_DB"` } // Validate проверяет обязательные поля. Возвращает C1 MissingField // (склеенную, все ошибки сразу) или C2 InvalidFormat. func (c *Config) Validate() error { var errs []error if c.Telegram.Token == "" { errs = append(errs, fmt.Errorf("%w: telegram.token", ErrMissingField)) } if c.Telegram.ChatID == "" { errs = append(errs, fmt.Errorf("%w: telegram.chat_id", ErrMissingField)) } return errors.Join(errs...) }