- Add a `--force` flag to the `push` command in both agent and MCP modules, allowing users to overwrite existing versions by deleting them before pushing. - Update the `PushOptions` struct to include the `Force` field, ensuring the functionality is integrated into the push logic. - Enhance E2E tests to validate the behavior of the force push feature, ensuring that existing versions are deleted as expected when the flag is used. - Introduce tests for packing directories to respect `.yaoignore` rules, improving file exclusion handling during the packaging process.
131 lines
3.2 KiB
Go
131 lines
3.2 KiB
Go
package mcp
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/yaoapp/yao/registry/manager/common"
|
|
)
|
|
|
|
// PushOptions configures the Push operation.
|
|
type PushOptions struct {
|
|
Version string
|
|
Force bool // delete existing version before push
|
|
}
|
|
|
|
// Push packages and uploads an MCP to the registry.
|
|
// Per DESIGN-MCP.md:
|
|
// 1. ID → path
|
|
// 2. Validate .mcp.yao exists
|
|
// 3. Derive scope/name, reject @local
|
|
// 4. Validate scripts are in scripts/{scope}/
|
|
// 5. Pack MCP dir + collect scripts from project root
|
|
// 6. Generate pkg.yao
|
|
// 7. Push to registry
|
|
func (m *Manager) Push(yaoID string, opts PushOptions) error {
|
|
if opts.Version == "" {
|
|
return fmt.Errorf("--version is required for push")
|
|
}
|
|
|
|
scope, name, err := common.IDFromYaoID(yaoID)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid MCP ID %q: %w", yaoID, err)
|
|
}
|
|
|
|
if common.IsLocalScope(scope) {
|
|
return fmt.Errorf("cannot push @local packages. Fork to your own scope first")
|
|
}
|
|
|
|
mcpDir := common.PackageDir(common.TypeMCP, scope, name, m.appRoot)
|
|
|
|
// Find .mcp.yao files
|
|
mcpYaoFiles, err := FindMCPYaoFiles(mcpDir)
|
|
if err != nil || len(mcpYaoFiles) == 0 {
|
|
return fmt.Errorf(".mcp.yao not found in %s", mcpDir)
|
|
}
|
|
|
|
// Validate script scope and collect scripts
|
|
allScripts := map[string]string{}
|
|
for _, mcpFile := range mcpYaoFiles {
|
|
if err := ValidateScriptScope(mcpFile, scope, m.appRoot); err != nil {
|
|
return err
|
|
}
|
|
scripts, err := CollectScripts(mcpFile, m.appRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for k, v := range scripts {
|
|
allScripts[k] = v
|
|
}
|
|
}
|
|
|
|
manifest := &common.PkgManifest{
|
|
Type: common.TypeMCP,
|
|
Scope: "@" + scope,
|
|
Name: name,
|
|
Version: opts.Version,
|
|
}
|
|
|
|
zipData, err := common.PackDir(mcpDir, manifest, allScripts)
|
|
if err != nil {
|
|
return fmt.Errorf("pack: %w", err)
|
|
}
|
|
|
|
regType := common.TypeToRegistryType(common.TypeMCP)
|
|
|
|
if opts.Force {
|
|
m.client.DeleteVersion(regType, "@"+scope, name, opts.Version)
|
|
}
|
|
|
|
result, err := m.client.Push(regType, "@"+scope, name, opts.Version, zipData)
|
|
if err != nil {
|
|
return fmt.Errorf("push: %w", err)
|
|
}
|
|
|
|
fmt.Printf("✓ Pushed %s@%s (digest: %s)\n", common.FormatPackageID(scope, name), result.Version, result.Digest)
|
|
|
|
// Report packed scripts
|
|
if len(allScripts) > 0 {
|
|
fmt.Printf(" Scripts packed:\n")
|
|
for scriptPath := range allScripts {
|
|
fmt.Printf(" %s\n", scriptPath)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// pushValidateMCPDir checks that the MCP directory structure is valid for push.
|
|
func pushValidateMCPDir(mcpDir string) error {
|
|
if _, err := os.Stat(mcpDir); err != nil {
|
|
return fmt.Errorf("MCP directory %s not found", mcpDir)
|
|
}
|
|
|
|
mcpFiles, err := FindMCPYaoFiles(mcpDir)
|
|
if err != nil || len(mcpFiles) == 0 {
|
|
return fmt.Errorf("no .mcp.yao files found in %s", mcpDir)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// listDirFiles lists all files under a directory relative to root.
|
|
func listDirFiles(dir string) ([]string, error) {
|
|
var files []string
|
|
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(dir, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files = append(files, filepath.ToSlash(rel))
|
|
return nil
|
|
})
|
|
return files, err
|
|
}
|