package manifest import ( "errors" "fmt" "os" "path/filepath" "strings" "github.com/BurntSushi/toml" "gitea.lclr.dev/AI/mcp-framework/update" ) const DefaultFile = "mcp.toml" type File struct { BinaryName string `toml:"binary_name"` DocsURL string `toml:"docs_url"` Update Update `toml:"update"` Build Build `toml:"build"` Environment Environment `toml:"environment"` SecretStore SecretStore `toml:"secret_store"` Profiles Profiles `toml:"profiles"` Bootstrap Bootstrap `toml:"bootstrap"` } type Update struct { SourceName string `toml:"source_name"` Driver string `toml:"driver"` Repository string `toml:"repository"` BaseURL string `toml:"base_url"` LatestReleaseURL string `toml:"latest_release_url"` AssetNameTemplate string `toml:"asset_name_template"` ChecksumAssetName string `toml:"checksum_asset_name"` ChecksumRequired bool `toml:"checksum_required"` TokenHeader string `toml:"token_header"` TokenPrefix string `toml:"token_prefix"` TokenEnvNames []string `toml:"token_env_names"` } type Build struct { MainPackage string `toml:"main_package"` OutputDir string `toml:"output_dir"` VersionVar string `toml:"version_var"` Targets []BuildTarget `toml:"targets"` } type BuildTarget struct { OS string `toml:"os"` Arch string `toml:"arch"` } type Environment struct { Known []string `toml:"known"` } type SecretStore struct { BackendPolicy string `toml:"backend_policy"` } type Profiles struct { Default string `toml:"default"` Known []string `toml:"known"` } type Bootstrap struct { Description string `toml:"description"` } type BootstrapMetadata struct { BinaryName string Description string DocsURL string DefaultProfile string Profiles []string } type ScaffoldMetadata struct { BinaryName string DocsURL string KnownEnvironmentVariables []string SecretStorePolicy string DefaultProfile string Profiles []string } func Find(startDir string) (string, error) { dir := strings.TrimSpace(startDir) if dir == "" { wd, err := os.Getwd() if err != nil { return "", fmt.Errorf("resolve working directory: %w", err) } dir = wd } absDir, err := filepath.Abs(dir) if err != nil { return "", fmt.Errorf("resolve manifest search path %q: %w", dir, err) } for { path := filepath.Join(absDir, DefaultFile) info, err := os.Stat(path) switch { case err == nil: if info.IsDir() { return "", fmt.Errorf("manifest path %q is a directory", path) } return path, nil case !errors.Is(err, os.ErrNotExist): return "", fmt.Errorf("stat manifest %q: %w", path, err) } parent := filepath.Dir(absDir) if parent == absDir { return "", fmt.Errorf("find manifest %q from %q: %w", DefaultFile, dir, os.ErrNotExist) } absDir = parent } } func Load(path string) (File, error) { data, err := os.ReadFile(path) if err != nil { return File{}, fmt.Errorf("read manifest %s: %w", path, err) } var file File if err := toml.Unmarshal(data, &file); err != nil { return File{}, fmt.Errorf("parse manifest %s: %w", path, err) } file.normalize() return file, nil } func LoadDefault(startDir string) (File, string, error) { path, err := Find(startDir) if err != nil { return File{}, "", err } file, err := Load(path) if err != nil { return File{}, "", err } return file, path, nil } func (f *File) normalize() { f.BinaryName = strings.TrimSpace(f.BinaryName) f.DocsURL = strings.TrimSpace(f.DocsURL) f.Update.normalize() f.Build.normalize() f.Environment.normalize() f.SecretStore.normalize() f.Profiles.normalize() f.Bootstrap.normalize() } func (u *Update) normalize() { u.SourceName = strings.TrimSpace(u.SourceName) u.Driver = strings.ToLower(strings.TrimSpace(u.Driver)) u.Repository = strings.Trim(strings.TrimSpace(u.Repository), "/") u.BaseURL = strings.TrimRight(strings.TrimSpace(u.BaseURL), "/") u.LatestReleaseURL = strings.TrimSpace(u.LatestReleaseURL) u.AssetNameTemplate = strings.TrimSpace(u.AssetNameTemplate) u.ChecksumAssetName = strings.TrimSpace(u.ChecksumAssetName) u.TokenHeader = strings.TrimSpace(u.TokenHeader) u.TokenPrefix = strings.TrimSpace(u.TokenPrefix) u.TokenEnvNames = normalizeStringList(u.TokenEnvNames) } func (b *Build) normalize() { b.MainPackage = strings.TrimSpace(b.MainPackage) b.OutputDir = strings.TrimSpace(b.OutputDir) b.VersionVar = strings.TrimSpace(b.VersionVar) b.Targets = normalizeBuildTargets(b.Targets) } func (e *Environment) normalize() { e.Known = normalizeStringList(e.Known) } func (s *SecretStore) normalize() { s.BackendPolicy = strings.TrimSpace(s.BackendPolicy) } func (p *Profiles) normalize() { p.Default = strings.TrimSpace(p.Default) p.Known = normalizeStringList(p.Known) } func (b *Bootstrap) normalize() { b.Description = strings.TrimSpace(b.Description) } func (u Update) ReleaseSource() update.ReleaseSource { u.normalize() return update.ReleaseSource{ Name: u.SourceName, Driver: u.Driver, Repository: u.Repository, BaseURL: u.BaseURL, LatestReleaseURL: u.LatestReleaseURL, AssetNameTemplate: u.AssetNameTemplate, ChecksumAssetName: u.ChecksumAssetName, ChecksumRequired: u.ChecksumRequired, TokenHeader: u.TokenHeader, TokenPrefix: u.TokenPrefix, TokenEnvNames: append([]string(nil), u.TokenEnvNames...), } } func (f File) BootstrapInfo() BootstrapMetadata { f.normalize() return BootstrapMetadata{ BinaryName: f.BinaryName, Description: f.Bootstrap.Description, DocsURL: f.DocsURL, DefaultProfile: f.Profiles.Default, Profiles: append([]string(nil), f.Profiles.Known...), } } func (f File) ScaffoldInfo() ScaffoldMetadata { f.normalize() return ScaffoldMetadata{ BinaryName: f.BinaryName, DocsURL: f.DocsURL, KnownEnvironmentVariables: append([]string(nil), f.Environment.Known...), SecretStorePolicy: f.SecretStore.BackendPolicy, DefaultProfile: f.Profiles.Default, Profiles: append([]string(nil), f.Profiles.Known...), } } func normalizeStringList(values []string) []string { normalized := values[:0] for _, value := range values { if trimmed := strings.TrimSpace(value); trimmed != "" { normalized = append(normalized, trimmed) } } return normalized } func normalizeBuildTargets(values []BuildTarget) []BuildTarget { if len(values) == 0 { return values } normalized := values[:0] seen := make(map[string]struct{}, len(values)) for _, value := range values { target := BuildTarget{ OS: strings.ToLower(strings.TrimSpace(value.OS)), Arch: strings.ToLower(strings.TrimSpace(value.Arch)), } if target.OS == "" || target.Arch == "" { continue } key := target.OS + "/" + target.Arch if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} normalized = append(normalized, target) } return normalized }