package agents import ( "fmt" "sort" "strings" "sync" ) // Registry holds the set of adapters `gortex init` will run. Callers // Register each adapter explicitly (from cmd/gortex/init.go) rather // than via sub-package init() hooks so that unit tests can build a // registry with a controlled subset without a global side-effect. // // Methods are safe for concurrent reads; Register should only be // called during startup. type Registry struct { mu sync.RWMutex adapters []Adapter byName map[string]Adapter } // NewRegistry returns an empty Registry. func NewRegistry() *Registry { return &Registry{byName: make(map[string]Adapter)} } // Register adds an adapter. Panics on duplicate names — duplicates // indicate a programming error (two packages registering the same // name), not a user mistake, so a panic is appropriate. func (r *Registry) Register(a Adapter) { r.mu.Lock() defer r.mu.Unlock() name := a.Name() if _, dup := r.byName[name]; dup { panic(fmt.Sprintf("agents.Registry: duplicate adapter name %q", name)) } r.byName[name] = a r.adapters = append(r.adapters, a) } // All returns adapters in registration order. Callers should not // mutate the returned slice. func (r *Registry) All() []Adapter { r.mu.RLock() defer r.mu.RUnlock() out := make([]Adapter, len(r.adapters)) copy(out, r.adapters) return out } // Names returns the sorted list of registered adapter names. Used by // the --agents flag's help text and the unknown-name error message. func (r *Registry) Names() []string { r.mu.RLock() defer r.mu.RUnlock() out := make([]string, 0, len(r.byName)) for name := range r.byName { out = append(out, name) } sort.Strings(out) return out } // Lookup returns the adapter with the given name, or nil when no // adapter is registered under that name. func (r *Registry) Lookup(name string) Adapter { r.mu.RLock() defer r.mu.RUnlock() return r.byName[name] } // Filter returns the subset of adapters selected by allow/skip lists. // allowCSV: // - "" (empty) or "auto" selects every registered adapter (the // default: let Detect decide which ones run) // - comma-separated list of names selects only those // - any unknown name produces an error // // skipCSV removes names from the selection after allowCSV applies. // Unknown skip names are also hard errors — silent skips mask typos. // // The returned slice preserves registration order so the user sees a // stable run sequence. func (r *Registry) Filter(allowCSV, skipCSV string) ([]Adapter, error) { r.mu.RLock() defer r.mu.RUnlock() allow := parseCSV(allowCSV) skip := parseCSV(skipCSV) // Validate names up front; we'd rather report all typos at once. var unknown []string for _, n := range allow { if n == "auto" { continue } if _, ok := r.byName[n]; !ok { unknown = append(unknown, n) } } for _, n := range skip { if _, ok := r.byName[n]; !ok { unknown = append(unknown, n) } } if len(unknown) > 0 { sort.Strings(unknown) return nil, fmt.Errorf("unknown agent name(s): %s; available: %s", strings.Join(unknown, ", "), strings.Join(r.sortedNamesLocked(), ", ")) } // Build the allowed set. Empty allowCSV or the "auto" sentinel // means "every registered adapter" — Detect() then decides which // ones actually run. var allowSet map[string]struct{} autoMode := len(allow) == 0 for _, n := range allow { if n == "auto" { autoMode = true continue } } if !autoMode { allowSet = make(map[string]struct{}, len(allow)) for _, n := range allow { allowSet[n] = struct{}{} } } skipSet := make(map[string]struct{}, len(skip)) for _, n := range skip { skipSet[n] = struct{}{} } out := make([]Adapter, 0, len(r.adapters)) for _, a := range r.adapters { name := a.Name() if allowSet != nil { if _, ok := allowSet[name]; !ok { continue } } if _, skipped := skipSet[name]; skipped { continue } out = append(out, a) } return out, nil } func (r *Registry) sortedNamesLocked() []string { out := make([]string, 0, len(r.byName)) for name := range r.byName { out = append(out, name) } sort.Strings(out) return out } // parseCSV splits a comma-separated list and trims whitespace. Empty // input and all-whitespace input yield a nil slice. func parseCSV(s string) []string { s = strings.TrimSpace(s) if s == "" { return nil } parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } return out }