2026-04-13 13:52:00 +00:00
|
|
|
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 {
|
2026-04-14 10:30:56 +00:00
|
|
|
BinaryName string `toml:"binary_name"`
|
|
|
|
|
DocsURL string `toml:"docs_url"`
|
|
|
|
|
Update Update `toml:"update"`
|
2026-04-14 16:02:07 +00:00
|
|
|
Build Build `toml:"build"`
|
2026-04-14 10:30:56 +00:00
|
|
|
Environment Environment `toml:"environment"`
|
|
|
|
|
SecretStore SecretStore `toml:"secret_store"`
|
|
|
|
|
Profiles Profiles `toml:"profiles"`
|
|
|
|
|
Bootstrap Bootstrap `toml:"bootstrap"`
|
2026-04-13 13:52:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Update struct {
|
2026-04-14 12:11:43 +00:00
|
|
|
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"`
|
2026-04-13 13:52:00 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 16:02:07 +00:00
|
|
|
type Build struct {
|
2026-04-15 07:09:39 +00:00
|
|
|
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"`
|
2026-04-14 16:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:30:56 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 13:52:00 +00:00
|
|
|
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() {
|
2026-04-14 10:30:56 +00:00
|
|
|
f.BinaryName = strings.TrimSpace(f.BinaryName)
|
|
|
|
|
f.DocsURL = strings.TrimSpace(f.DocsURL)
|
2026-04-13 13:52:00 +00:00
|
|
|
f.Update.normalize()
|
2026-04-14 16:02:07 +00:00
|
|
|
f.Build.normalize()
|
2026-04-14 10:30:56 +00:00
|
|
|
f.Environment.normalize()
|
|
|
|
|
f.SecretStore.normalize()
|
|
|
|
|
f.Profiles.normalize()
|
|
|
|
|
f.Bootstrap.normalize()
|
2026-04-13 13:52:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (u *Update) normalize() {
|
|
|
|
|
u.SourceName = strings.TrimSpace(u.SourceName)
|
2026-04-14 12:11:43 +00:00
|
|
|
u.Driver = strings.ToLower(strings.TrimSpace(u.Driver))
|
|
|
|
|
u.Repository = strings.Trim(strings.TrimSpace(u.Repository), "/")
|
2026-04-13 13:52:00 +00:00
|
|
|
u.BaseURL = strings.TrimRight(strings.TrimSpace(u.BaseURL), "/")
|
|
|
|
|
u.LatestReleaseURL = strings.TrimSpace(u.LatestReleaseURL)
|
2026-04-14 12:11:43 +00:00
|
|
|
u.AssetNameTemplate = strings.TrimSpace(u.AssetNameTemplate)
|
|
|
|
|
u.ChecksumAssetName = strings.TrimSpace(u.ChecksumAssetName)
|
2026-04-13 13:52:00 +00:00
|
|
|
u.TokenHeader = strings.TrimSpace(u.TokenHeader)
|
2026-04-14 12:11:43 +00:00
|
|
|
u.TokenPrefix = strings.TrimSpace(u.TokenPrefix)
|
2026-04-14 10:30:56 +00:00
|
|
|
u.TokenEnvNames = normalizeStringList(u.TokenEnvNames)
|
|
|
|
|
}
|
2026-04-13 13:52:00 +00:00
|
|
|
|
2026-04-14 16:02:07 +00:00
|
|
|
func (b *Build) normalize() {
|
|
|
|
|
b.MainPackage = strings.TrimSpace(b.MainPackage)
|
|
|
|
|
b.OutputDir = strings.TrimSpace(b.OutputDir)
|
|
|
|
|
b.VersionVar = strings.TrimSpace(b.VersionVar)
|
2026-04-15 07:09:39 +00:00
|
|
|
b.Targets = normalizeBuildTargets(b.Targets)
|
2026-04-14 16:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 10:30:56 +00:00
|
|
|
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)
|
2026-04-13 13:52:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (u Update) ReleaseSource() update.ReleaseSource {
|
|
|
|
|
u.normalize()
|
|
|
|
|
|
|
|
|
|
return update.ReleaseSource{
|
2026-04-14 12:11:43 +00:00
|
|
|
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...),
|
2026-04-13 13:52:00 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-14 10:30:56 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2026-04-15 07:09:39 +00:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|