mirror of
https://github.com/tiennm99/claude-code-routine-trigger.git
synced 2026-10-14 18:18:24 +00:00
feat: implement core daemon (config, fire client, scheduler)
This commit is contained in:
1 parent
981ec3255e
commit
9c4d7a532f
6 files changed
+384
-1
No files matched your search
@@ -0,0 +1,104 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
const defaultTextTemplate = "Scheduled trigger at {{.LocalTime}}"
|
||||
|
||||
// Config is the validated runtime configuration assembled from env vars.
|
||||
type Config struct {
|
||||
FireURL string
|
||||
Token string
|
||||
Schedules []string
|
||||
Location *time.Location
|
||||
Template *template.Template
|
||||
LogLevel string
|
||||
}
|
||||
|
||||
// Load reads env vars and returns a fully validated Config or an error.
|
||||
// All required vars must be present and all parsed values must pass validation.
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{}
|
||||
|
||||
cfg.FireURL = strings.TrimSpace(os.Getenv("ROUTINE_FIRE_URL"))
|
||||
if cfg.FireURL == "" {
|
||||
return nil, errors.New("missing required env var ROUTINE_FIRE_URL")
|
||||
}
|
||||
|
||||
cfg.Token = strings.TrimSpace(os.Getenv("ROUTINE_FIRE_TOKEN"))
|
||||
if cfg.Token == "" {
|
||||
return nil, errors.New("missing required env var ROUTINE_FIRE_TOKEN")
|
||||
}
|
||||
|
||||
rawCron := os.Getenv("CRON_SCHEDULE")
|
||||
schedules, err := parseSchedules(rawCron)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.Schedules = schedules
|
||||
|
||||
tz := strings.TrimSpace(os.Getenv("TZ"))
|
||||
if tz == "" {
|
||||
cfg.Location = time.UTC
|
||||
} else {
|
||||
loc, err := time.LoadLocation(tz)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid TZ %q: %w", tz, err)
|
||||
}
|
||||
cfg.Location = loc
|
||||
}
|
||||
|
||||
rawTpl := os.Getenv("TEXT_TEMPLATE")
|
||||
if rawTpl == "" {
|
||||
rawTpl = defaultTextTemplate
|
||||
}
|
||||
tpl, err := template.New("text").Parse(rawTpl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid TEXT_TEMPLATE: %w", err)
|
||||
}
|
||||
cfg.Template = tpl
|
||||
|
||||
cfg.LogLevel = strings.TrimSpace(os.Getenv("LOG_LEVEL"))
|
||||
if cfg.LogLevel == "" {
|
||||
cfg.LogLevel = "info"
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
var scheduleSeparators = regexp.MustCompile(`[;\n]+`)
|
||||
|
||||
// parseSchedules splits CRON_SCHEDULE on `;` or newlines, trims, drops empties,
|
||||
// and validates each expression against the standard 5-field parser.
|
||||
func parseSchedules(raw string) ([]string, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, errors.New("missing required env var CRON_SCHEDULE")
|
||||
}
|
||||
parts := scheduleSeparators.Split(raw, -1)
|
||||
parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
|
||||
out := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
s := strings.TrimSpace(p)
|
||||
if s == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := parser.Parse(s); err != nil {
|
||||
return nil, fmt.Errorf("invalid cron %q: %w", s, err)
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, errors.New("CRON_SCHEDULE has no non-empty expressions")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in new issue
Block a user