a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
147 lines
4.5 KiB
Go
147 lines
4.5 KiB
Go
package mcp
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/zzet/gortex/internal/persistence"
|
|
"github.com/zzet/gortex/internal/platform"
|
|
)
|
|
|
|
// SavedScope is a named, persisted set of repository prefixes — a reusable
|
|
// slice of a multi-repo workspace that query tools accept by name instead
|
|
// of re-specifying repo filters on every call.
|
|
type SavedScope struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Repos []string `json:"repos"`
|
|
// Paths narrows a scope below the repository grain to a set of
|
|
// sub-paths -- the monorepo-service slice (services/billing,
|
|
// libs/auth). Anchored, slash-segment-normalised prefixes
|
|
// relative to each repo root. Empty (the back-compatible default)
|
|
// means the scope is repo-level only -- an existing scopes.json
|
|
// without this field decodes cleanly.
|
|
Paths []string `json:"paths,omitempty"`
|
|
}
|
|
|
|
// scopeStore is a small registry of SavedScopes backed by the SQLite
|
|
// sidecar DB. It survives daemon restarts. Scopes are global (not
|
|
// repo-scoped). The in-memory byName map mirrors the scopes table so
|
|
// reads stay lock-cheap; mutations write through to the sidecar. All
|
|
// exported methods are safe for concurrent use.
|
|
type scopeStore struct {
|
|
mu sync.Mutex
|
|
sidecar *persistence.SidecarStore
|
|
byName map[string]SavedScope
|
|
}
|
|
|
|
// scopesFilePath returns the legacy on-disk location of the saved-scope
|
|
// store, honouring GORTEX_SCOPES_PATH (used by tests) over the cache
|
|
// default. The sidecar DB lives next to it (<dir>/sidecar.sqlite); a
|
|
// pre-existing scopes.json at this path is imported once, then renamed
|
|
// to scopes.json.bak.
|
|
func scopesFilePath() string {
|
|
if p := strings.TrimSpace(os.Getenv("GORTEX_SCOPES_PATH")); p != "" {
|
|
return p
|
|
}
|
|
return filepath.Join(platform.OSCacheDir(), "scopes.json")
|
|
}
|
|
|
|
// newScopeStore builds a store whose sidecar DB lives next to the given
|
|
// legacy scopes.json path. Any scopes.json present is imported once,
|
|
// then the in-memory map is hydrated from the sidecar. A nil sidecar
|
|
// (open failure) yields an in-memory-only store.
|
|
func newScopeStore(legacyPath string) *scopeStore {
|
|
sidecarPath := persistence.DefaultSidecarPath(filepath.Dir(legacyPath))
|
|
sidecar, _ := persistence.OpenSidecar(sidecarPath)
|
|
return newScopeStoreFromSidecar(sidecar, legacyPath)
|
|
}
|
|
|
|
// newScopeStoreFromSidecar builds a scope store bound to an already-open
|
|
// sidecar, importing legacyPath/scopes.json once. Used by the daemon
|
|
// path where the sidecar is opened once and shared.
|
|
func newScopeStoreFromSidecar(sidecar *persistence.SidecarStore, legacyPath string) *scopeStore {
|
|
st := &scopeStore{sidecar: sidecar, byName: map[string]SavedScope{}}
|
|
if sidecar != nil {
|
|
_ = sidecar.MigrateLegacyScopes(legacyPath)
|
|
if rows, err := sidecar.LoadScopes(); err == nil {
|
|
for _, r := range rows {
|
|
if r.Name != "" {
|
|
st.byName[r.Name] = SavedScope{Name: r.Name, Description: r.Description, Repos: r.Repos, Paths: r.Paths}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return st
|
|
}
|
|
|
|
func (st *scopeStore) get(name string) (SavedScope, bool) {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
sc, ok := st.byName[name]
|
|
return sc, ok
|
|
}
|
|
|
|
func (st *scopeStore) list() []SavedScope {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
out := make([]SavedScope, 0, len(st.byName))
|
|
for _, sc := range st.byName {
|
|
out = append(out, sc)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out
|
|
}
|
|
|
|
func (st *scopeStore) put(sc SavedScope) error {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
st.byName[sc.Name] = sc
|
|
if st.sidecar == nil {
|
|
return nil
|
|
}
|
|
return st.sidecar.UpsertScope(persistence.ScopeRow{
|
|
Name: sc.Name, Description: sc.Description, Repos: sc.Repos, Paths: sc.Paths,
|
|
})
|
|
}
|
|
|
|
func (st *scopeStore) remove(name string) (bool, error) {
|
|
st.mu.Lock()
|
|
defer st.mu.Unlock()
|
|
if _, ok := st.byName[name]; !ok {
|
|
return false, nil
|
|
}
|
|
delete(st.byName, name)
|
|
if st.sidecar == nil {
|
|
return true, nil
|
|
}
|
|
return true, st.sidecar.DeleteScope(name)
|
|
}
|
|
|
|
// scopeStoreOrInit lazily constructs the per-server saved-scope store.
|
|
func (s *Server) scopeStoreOrInit() *scopeStore {
|
|
s.scopesOnce.Do(func() {
|
|
s.scopes = newScopeStore(scopesFilePath())
|
|
})
|
|
return s.scopes
|
|
}
|
|
|
|
// lookupScope returns the named saved scope.
|
|
func (s *Server) lookupScope(name string) (SavedScope, bool) {
|
|
return s.scopeStoreOrInit().get(name)
|
|
}
|
|
|
|
// scopeRepoSet expands a saved scope into a repo-prefix allow-set.
|
|
func (s *Server) scopeRepoSet(sc SavedScope) map[string]bool {
|
|
out := make(map[string]bool, len(sc.Repos))
|
|
for _, r := range sc.Repos {
|
|
if r = strings.TrimSpace(r); r != "" {
|
|
out[r] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|