310 lines
8.5 KiB
Go
310 lines
8.5 KiB
Go
package manifest
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
"gitea.lclr.dev/AI/mcp-framework/update"
|
|
)
|
|
|
|
const DefaultFile = "mcp.toml"
|
|
const EmbeddedSource = "embedded:mcp.toml"
|
|
|
|
type File struct {
|
|
BinaryName string `toml:"binary_name"`
|
|
DocsURL string `toml:"docs_url"`
|
|
Update Update `toml:"update"`
|
|
Environment Environment `toml:"environment"`
|
|
SecretStore SecretStore `toml:"secret_store"`
|
|
Profiles Profiles `toml:"profiles"`
|
|
Bootstrap Bootstrap `toml:"bootstrap"`
|
|
Config Config `toml:"config"`
|
|
}
|
|
|
|
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"`
|
|
SignatureAssetName string `toml:"signature_asset_name"`
|
|
SignatureRequired bool `toml:"signature_required"`
|
|
SignaturePublicKey string `toml:"signature_public_key"`
|
|
SignaturePublicKeyEnvNames []string `toml:"signature_public_key_env_names"`
|
|
TokenHeader string `toml:"token_header"`
|
|
TokenPrefix string `toml:"token_prefix"`
|
|
TokenEnvNames []string `toml:"token_env_names"`
|
|
}
|
|
|
|
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 Config struct {
|
|
Fields []ConfigField `toml:"fields"`
|
|
}
|
|
|
|
type ConfigField struct {
|
|
Name string `toml:"name"`
|
|
Flag string `toml:"flag"`
|
|
Env string `toml:"env"`
|
|
ConfigKey string `toml:"config_key"`
|
|
SecretKeyTemplate string `toml:"secret_key_template"`
|
|
Type string `toml:"type"`
|
|
Label string `toml:"label"`
|
|
Default string `toml:"default"`
|
|
Required bool `toml:"required"`
|
|
Sources []string `toml:"sources"`
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return parse(data, path)
|
|
}
|
|
|
|
func LoadEmbedded(content string) (File, string, error) {
|
|
trimmed := strings.TrimSpace(content)
|
|
if trimmed == "" {
|
|
return File{}, "", os.ErrNotExist
|
|
}
|
|
|
|
file, err := parse([]byte(trimmed), EmbeddedSource)
|
|
if err != nil {
|
|
return File{}, "", err
|
|
}
|
|
|
|
return file, EmbeddedSource, nil
|
|
}
|
|
|
|
func LoadDefaultOrEmbedded(startDir, embeddedContent string) (File, string, error) {
|
|
file, path, err := LoadDefault(startDir)
|
|
if err == nil {
|
|
return file, path, nil
|
|
}
|
|
if !errors.Is(err, os.ErrNotExist) {
|
|
return File{}, "", err
|
|
}
|
|
|
|
return LoadEmbedded(embeddedContent)
|
|
}
|
|
|
|
func parse(data []byte, source string) (File, error) {
|
|
var file File
|
|
if err := toml.Unmarshal(data, &file); err != nil {
|
|
return File{}, fmt.Errorf("parse manifest %s: %w", source, 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.Environment.normalize()
|
|
f.SecretStore.normalize()
|
|
f.Profiles.normalize()
|
|
f.Bootstrap.normalize()
|
|
f.Config.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.SignatureAssetName = strings.TrimSpace(u.SignatureAssetName)
|
|
u.SignaturePublicKey = strings.TrimSpace(u.SignaturePublicKey)
|
|
u.SignaturePublicKeyEnvNames = normalizeStringList(u.SignaturePublicKeyEnvNames)
|
|
u.TokenHeader = strings.TrimSpace(u.TokenHeader)
|
|
u.TokenPrefix = strings.TrimSpace(u.TokenPrefix)
|
|
u.TokenEnvNames = normalizeStringList(u.TokenEnvNames)
|
|
}
|
|
|
|
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 (c *Config) normalize() {
|
|
for i := range c.Fields {
|
|
c.Fields[i].normalize()
|
|
}
|
|
}
|
|
|
|
func (f *ConfigField) normalize() {
|
|
f.Name = strings.TrimSpace(f.Name)
|
|
f.Flag = strings.TrimSpace(f.Flag)
|
|
f.Env = strings.TrimSpace(f.Env)
|
|
f.ConfigKey = strings.TrimSpace(f.ConfigKey)
|
|
f.SecretKeyTemplate = strings.TrimSpace(f.SecretKeyTemplate)
|
|
f.Type = strings.ToLower(strings.TrimSpace(f.Type))
|
|
f.Label = strings.TrimSpace(f.Label)
|
|
f.Default = strings.TrimSpace(f.Default)
|
|
f.Sources = normalizeStringList(f.Sources)
|
|
}
|
|
|
|
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,
|
|
SignatureAssetName: u.SignatureAssetName,
|
|
SignatureRequired: u.SignatureRequired,
|
|
SignaturePublicKey: u.SignaturePublicKey,
|
|
SignaturePublicKeyEnvNames: append([]string(nil), u.SignaturePublicKeyEnvNames...),
|
|
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
|
|
}
|