yao/registry/manager/mcp/update.go
Max 44af6ba759 Enhance command structure with MCP and Robot functionalities
- Introduce new MCP and Robot command groups in the CLI, allowing for better organization of package management commands.
- Add corresponding command descriptions for MCP and Robot functionalities to improve user guidance.
- Update the agent command group to include additional commands for enhanced agent management.
- Modify .gitignore to exclude specific design markdown files from version control.
- Add environment variable for test application path in GitHub workflows to streamline testing setup.
2026-03-03 08:11:43 +08:00

200 lines
5.2 KiB
Go

package mcp
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/yaoapp/yao/registry/manager/common"
)
// UpdateOptions configures the Update operation.
type UpdateOptions struct {
Version string
}
// Update performs a hash-based safe update for an MCP package.
// Same strategy as agent update but also handles scripts under project root.
func (m *Manager) Update(pkgID string, opts UpdateOptions) error {
if opts.Version == "" {
opts.Version = "latest"
}
scope, name, err := common.ParsePackageID(pkgID)
if err != nil {
return err
}
lf, err := common.LoadLockfile(m.appRoot)
if err != nil {
return err
}
existing, ok := lf.GetPackage(pkgID)
if !ok {
return fmt.Errorf("package %s is not installed", pkgID)
}
if !existing.IsManaged() {
return fmt.Errorf("package %s is forked (from %s) and not managed by registry", pkgID, existing.ForkedFrom)
}
regType := common.TypeToRegistryType(common.TypeMCP)
zipData, digest, err := m.client.Pull(regType, "@"+scope, name, opts.Version)
if err != nil {
return fmt.Errorf("pull %s: %w", pkgID, err)
}
manifest, err := common.ReadManifest(zipData)
if err != nil {
return fmt.Errorf("read manifest: %w", err)
}
// Check required_by compatibility
if len(existing.RequiredBy) > 0 {
var warnings []string
for _, depID := range existing.RequiredBy {
depPkg, depOK := lf.GetPackage(depID)
if !depOK {
continue
}
if constraint, has := depPkg.Dependencies[pkgID]; has {
if !common.VersionSatisfies(manifest.Version, constraint) {
warnings = append(warnings, fmt.Sprintf(" %s requires %s ← incompatible", depID, constraint))
}
}
}
if len(warnings) > 0 {
msg := fmt.Sprintf("%s is depended on by:\n%s\nContinue update?", pkgID, strings.Join(warnings, "\n"))
if !m.prompter.Confirm(msg) {
return fmt.Errorf("update aborted by user")
}
}
}
// Unpack new version to temp
tempDir, err := os.MkdirTemp("", "yao-mcp-update-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
newFileList, err := common.UnpackTo(zipData, tempDir)
if err != nil {
return err
}
mcpRelDir := common.PackageDirRel(common.TypeMCP, scope, name)
destDir := common.PackageDir(common.TypeMCP, scope, name, m.appRoot)
// Build new file set with full relative paths
newFileMap := map[string]string{} // fullRelPath → temp file path
for _, f := range newFileList {
if strings.HasPrefix(f, "scripts/") {
newFileMap[f] = filepath.Join(tempDir, f)
} else {
fullRel := mcpRelDir + "/" + f
newFileMap[fullRel] = filepath.Join(tempDir, f)
}
}
newHashes := map[string]string{}
// Process each new file
for fullRel, tempPath := range newFileMap {
newContent, err := os.ReadFile(tempPath)
if err != nil {
return err
}
newHash := common.HashBytes(newContent)
// Determine local path
var localPath string
if strings.HasPrefix(fullRel, "scripts/") {
localPath = filepath.Join(m.appRoot, fullRel)
} else {
relInMCP := strings.TrimPrefix(fullRel, mcpRelDir+"/")
localPath = filepath.Join(destDir, relInMCP)
}
oldHash, wasTracked := existing.Files[fullRel]
if !wasTracked {
if err := writeFileTo(localPath, newContent); err != nil {
return err
}
fmt.Printf("+ %s — new file, added\n", filepath.Base(fullRel))
newHashes[fullRel] = newHash
continue
}
localHash, err := common.HashFile(localPath)
if err != nil {
if err := writeFileTo(localPath, newContent); err != nil {
return err
}
fmt.Printf("✓ %s — restored (was missing locally)\n", filepath.Base(fullRel))
newHashes[fullRel] = newHash
continue
}
if localHash == oldHash {
if err := writeFileTo(localPath, newContent); err != nil {
return err
}
fmt.Printf("✓ %s — unmodified, updated\n", filepath.Base(fullRel))
newHashes[fullRel] = newHash
} else {
newPath := localPath + ".new"
if err := writeFileTo(newPath, newContent); err != nil {
return err
}
fmt.Printf("✗ %s — locally modified, skipped (new version → .new)\n", filepath.Base(fullRel))
newHashes[fullRel] = newHash
}
}
// Handle deleted files
for oldFile, oldHash := range existing.Files {
if _, inNew := newFileMap[oldFile]; inNew {
continue
}
var localPath string
if strings.HasPrefix(oldFile, "scripts/") {
localPath = filepath.Join(m.appRoot, oldFile)
} else {
relInMCP := strings.TrimPrefix(oldFile, mcpRelDir+"/")
localPath = filepath.Join(destDir, relInMCP)
}
localHash, err := common.HashFile(localPath)
if err != nil {
continue
}
if localHash == oldHash {
os.Remove(localPath)
fmt.Printf("- %s — removed\n", filepath.Base(oldFile))
} else {
fmt.Printf("⚠ %s — locally modified, kept\n", filepath.Base(oldFile))
}
}
existing.Version = manifest.Version
existing.Integrity = digest
existing.Dependencies = manifest.Dependencies
existing.Files = newHashes
lf.SetPackage(pkgID, existing)
if err := common.SaveLockfile(m.appRoot, lf); err != nil {
return err
}
fmt.Printf("✓ Updated %s to %s\n", pkgID, manifest.Version)
return nil
}
func writeFileTo(path string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
return os.WriteFile(path, content, 0644)
}