Files
2dph/bin/watch/watch.go
T
eSliderandGitHub 20b78a9a20
Tests / Test (push) Failing after 5s
Tests / Release (semver) (push) Skipped
feat: brain/index.go shebang; mail import is not a brain write (D14). (#12)
Commands live at bin/brain/{index,get,stats,eval,watch}.go and
bin/mail/import.go, bin/markdown/import.go, bin/postgres/query.go.
Python remains the Ladybug write worker. index_mail is a deprecation
shim that rebuilds via --with-mail.
2026-08-13 17:46:25 +01:00

106 lines
2.7 KiB
Go

// Package watch polls corpus directories for changes and re-runs brain/index.
//
// Port of the former bin/kb-watch bash script to an importable, testable Go
// package. Polls file mtimes (no inotify deps); cheap and reliable.
package watch
import (
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
// Options controls the polling loop. Zero value uses defaults.
type Options struct {
Dirs []string
Interval time.Duration
// IndexCmd is the index command template. %s is replaced by the repo
// root (from KB_ROOT). Defaults to `python3 <root>/bin/kb/index --with-mail`.
IndexCmd string
}
// Run blocks forever polling Dirs (defaults: KB_WATCH_DIRS or /corpus) every
// Interval (default 30s) and re-indexing when files change. KB_ROOT names the
// repo root used to locate bin/kb/index.
func Run(args []string) {
opts := fromEnv(args)
root, _ := os.Getwd()
if r := os.Getenv("KB_ROOT"); r != "" {
root = r
}
log.Printf("watch: dirs=%v interval=%s root=%s", opts.Dirs, opts.Interval, root)
var last string
for {
if flag := Stamp(opts.Dirs); flag != "" && flag != last {
last = flag
reindex(opts.IndexCmd, root)
}
time.Sleep(opts.Interval)
}
}
func fromEnv(args []string) Options {
opts := Options{Interval: 30 * time.Second}
if raw := os.Getenv("KB_WATCH_INTERVAL"); raw != "" {
if n, err := strconv.Atoi(raw); err == nil && n > 0 {
opts.Interval = time.Duration(n) * time.Second
}
}
defDirs := "/corpus"
if raw := os.Getenv("KB_WATCH_DIRS"); raw != "" {
defDirs = raw
}
if len(args) > 0 {
opts.Dirs = args
} else {
for _, d := range strings.Split(defDirs, " ") {
if d != "" {
opts.Dirs = append(opts.Dirs, d)
}
}
}
pys := os.Getenv("KB_PY")
if pys == "" {
pys = "python3"
}
opts.IndexCmd = pys + " <root>/bin/kb/index --with-mail"
return opts
}
// Stamp returns a rolling fingerprint (newest mtime under dirs) that changes
// whenever any corpus file is touched. Empty when no files found.
func Stamp(dirs []string) string {
var newest time.Time
for _, dir := range dirs {
_ = filepath.WalkDir(dir, func(path string, _ os.DirEntry, err error) error {
if err != nil {
return nil
}
if info, e := os.Stat(path); e == nil && info.ModTime().After(newest) {
newest = info.ModTime()
}
return nil
})
}
if newest.IsZero() {
return ""
}
return strconv.FormatInt(newest.UnixNano(), 10)
}
func reindex(template, root string) {
cmd := strings.ReplaceAll(template, "<root>", root)
parts := strings.Fields(cmd)
c := exec.Command(parts[0], parts[1:]...)
out, err := c.CombinedOutput()
if err != nil {
log.Printf("watch: index failed: %v\n%s", err, out)
} else {
log.Printf("watch: re-indexed")
}
}