Implement force push option for agent and MCP commands
- 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.
This commit is contained in:
parent
b4a9132dae
commit
dae35cfe86
8 changed files with 261 additions and 5 deletions
|
|
@ -21,6 +21,7 @@ var PushCmd = &cobra.Command{
|
||||||
|
|
||||||
yaoID := args[0]
|
yaoID := args[0]
|
||||||
version, _ := cmd.Flags().GetString("version")
|
version, _ := cmd.Flags().GetString("version")
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
client := registry.New(config.Conf.Registry,
|
client := registry.New(config.Conf.Registry,
|
||||||
registry.WithAuth(
|
registry.WithAuth(
|
||||||
|
|
@ -32,6 +33,7 @@ var PushCmd = &cobra.Command{
|
||||||
mgr := agentmgr.New(client, config.Conf.Root, nil)
|
mgr := agentmgr.New(client, config.Conf.Root, nil)
|
||||||
if err := mgr.Push(yaoID, agentmgr.PushOptions{
|
if err := mgr.Push(yaoID, agentmgr.PushOptions{
|
||||||
Version: version,
|
Version: version,
|
||||||
|
Force: force,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -41,6 +43,7 @@ var PushCmd = &cobra.Command{
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
PushCmd.Flags().StringP("version", "v", "", L("Package version (required)"))
|
PushCmd.Flags().StringP("version", "v", "", L("Package version (required)"))
|
||||||
|
PushCmd.Flags().Bool("force", false, L("Overwrite existing version"))
|
||||||
PushCmd.PersistentFlags().StringVarP(&appPath, "app", "a", "", L("Application directory"))
|
PushCmd.PersistentFlags().StringVarP(&appPath, "app", "a", "", L("Application directory"))
|
||||||
PushCmd.PersistentFlags().StringVarP(&envFile, "env", "e", "", L("Environment file"))
|
PushCmd.PersistentFlags().StringVarP(&envFile, "env", "e", "", L("Environment file"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ var PushCmd = &cobra.Command{
|
||||||
Boot()
|
Boot()
|
||||||
yaoID := args[0]
|
yaoID := args[0]
|
||||||
version, _ := cmd.Flags().GetString("version")
|
version, _ := cmd.Flags().GetString("version")
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
client := registry.New(config.Conf.Registry,
|
client := registry.New(config.Conf.Registry,
|
||||||
registry.WithAuth(
|
registry.WithAuth(
|
||||||
|
|
@ -31,6 +32,7 @@ var PushCmd = &cobra.Command{
|
||||||
mgr := mcpmgr.New(client, config.Conf.Root, nil)
|
mgr := mcpmgr.New(client, config.Conf.Root, nil)
|
||||||
if err := mgr.Push(yaoID, mcpmgr.PushOptions{
|
if err := mgr.Push(yaoID, mcpmgr.PushOptions{
|
||||||
Version: version,
|
Version: version,
|
||||||
|
Force: force,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -40,6 +42,7 @@ var PushCmd = &cobra.Command{
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
PushCmd.Flags().StringP("version", "v", "", L("Package version (required)"))
|
PushCmd.Flags().StringP("version", "v", "", L("Package version (required)"))
|
||||||
|
PushCmd.Flags().Bool("force", false, L("Overwrite existing version"))
|
||||||
PushCmd.PersistentFlags().StringVarP(&appPath, "app", "a", "", L("Application directory"))
|
PushCmd.PersistentFlags().StringVarP(&appPath, "app", "a", "", L("Application directory"))
|
||||||
PushCmd.PersistentFlags().StringVarP(&envFile, "env", "e", "", L("Environment file"))
|
PushCmd.PersistentFlags().StringVarP(&envFile, "env", "e", "", L("Environment file"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,13 @@ func mockRegistryServer(packages map[string][]byte) *httptest.Server {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete: DELETE /v1/{type}/{scope}/{name}/{version}
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Push: PUT /v1/{type}/{scope}/{name}/{version}
|
// Push: PUT /v1/{type}/{scope}/{name}/{version}
|
||||||
if r.Method == http.MethodPut {
|
if r.Method == http.MethodPut {
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
@ -425,6 +432,52 @@ func TestPushLocalScope(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPushForce(t *testing.T) {
|
||||||
|
appRoot := t.TempDir()
|
||||||
|
|
||||||
|
assistantDir := filepath.Join(appRoot, "assistants", "max", "my-agent")
|
||||||
|
os.MkdirAll(assistantDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(assistantDir, "package.yao"), []byte(`{"name":"my-agent"}`), 0644)
|
||||||
|
|
||||||
|
var deleteCalled bool
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/.well-known/yao-registry" {
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"registry": map[string]string{"version": "1.0.0", "api": "/v1"},
|
||||||
|
"types": []string{"assistants"},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodDelete {
|
||||||
|
deleteCalled = true
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{"status": "deleted"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"type": "assistants", "scope": "@max",
|
||||||
|
"name": "my-agent", "version": "1.0.0", "digest": "sha256-forced",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := registry.New(srv.URL, registry.WithAuth("u", "p"))
|
||||||
|
mgr := New(client, appRoot, &common.AutoConfirmPrompter{})
|
||||||
|
|
||||||
|
err := mgr.Push("max.my-agent", PushOptions{Version: "1.0.0", Force: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Force push failed: %v", err)
|
||||||
|
}
|
||||||
|
if !deleteCalled {
|
||||||
|
t.Error("expected DELETE to be called before PUT when Force=true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPushNoVersion(t *testing.T) {
|
func TestPushNoVersion(t *testing.T) {
|
||||||
appRoot := t.TempDir()
|
appRoot := t.TempDir()
|
||||||
srv := mockRegistryServer(nil)
|
srv := mockRegistryServer(nil)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
// PushOptions configures the Push operation.
|
// PushOptions configures the Push operation.
|
||||||
type PushOptions struct {
|
type PushOptions struct {
|
||||||
Version string // required semver
|
Version string // required semver
|
||||||
|
Force bool // delete existing version before push
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push packages and uploads an assistant to the registry.
|
// Push packages and uploads an assistant to the registry.
|
||||||
|
|
@ -66,8 +67,14 @@ func (m *Manager) Push(yaoID string, opts PushOptions) error {
|
||||||
return fmt.Errorf("pack: %w", err)
|
return fmt.Errorf("pack: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to registry
|
|
||||||
regType := common.TypeToRegistryType(common.TypeAssistant)
|
regType := common.TypeToRegistryType(common.TypeAssistant)
|
||||||
|
|
||||||
|
// Force: delete existing version first (ignore 404)
|
||||||
|
if opts.Force {
|
||||||
|
m.client.DeleteVersion(regType, "@"+scope, name, opts.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push to registry
|
||||||
result, err := m.client.Push(regType, "@"+scope, name, opts.Version, zipData)
|
result, err := m.client.Push(regType, "@"+scope, name, opts.Version, zipData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("push: %w", err)
|
return fmt.Errorf("push: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,13 @@ func TestE2EAgent_SingleDepLifecycle(t *testing.T) {
|
||||||
t.Fatalf("Push MCP: %v", err)
|
t.Fatalf("Push MCP: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify .yaoignore fixtures exist in source before push
|
||||||
|
srcAgent := filepath.Join(devApp, "assistants", testScope, "registry-agent")
|
||||||
|
requireFileExists(t, filepath.Join(srcAgent, ".yaoignore"))
|
||||||
|
requireFileExists(t, filepath.Join(srcAgent, "dev-notes.md"))
|
||||||
|
requireFileExists(t, filepath.Join(srcAgent, "wireframe.sketch"))
|
||||||
|
requireFileExists(t, filepath.Join(srcAgent, "debug", "trace.log"))
|
||||||
|
|
||||||
// Push agent
|
// Push agent
|
||||||
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
|
agentMgr := agentmgr.New(c, devApp, &common.AutoConfirmPrompter{})
|
||||||
if err := agentMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
|
if err := agentMgr.Push(testScope+".registry-agent", agentmgr.PushOptions{Version: "1.0.0"}); err != nil {
|
||||||
|
|
@ -56,6 +63,12 @@ func TestE2EAgent_SingleDepLifecycle(t *testing.T) {
|
||||||
requireFileExists(t, filepath.Join(agentDir, "package.yao"))
|
requireFileExists(t, filepath.Join(agentDir, "package.yao"))
|
||||||
requireFileContains(t, filepath.Join(agentDir, "prompts.yml"), "registry E2E testing")
|
requireFileContains(t, filepath.Join(agentDir, "prompts.yml"), "registry E2E testing")
|
||||||
|
|
||||||
|
// .yaoignore: excluded files must NOT appear in the installed package
|
||||||
|
requireFileNotExists(t, filepath.Join(agentDir, ".yaoignore"))
|
||||||
|
requireFileNotExists(t, filepath.Join(agentDir, "dev-notes.md"))
|
||||||
|
requireFileNotExists(t, filepath.Join(agentDir, "wireframe.sketch"))
|
||||||
|
requireFileNotExists(t, filepath.Join(agentDir, "debug", "trace.log"))
|
||||||
|
|
||||||
// Lockfile: agent entry
|
// Lockfile: agent entry
|
||||||
agentPkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-agent")
|
agentPkg := requireLockfileHas(t, installApp, "@"+testScope+"/registry-agent")
|
||||||
if agentPkg.Version != "1.0.0" {
|
if agentPkg.Version != "1.0.0" {
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,30 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yaoapp/gou/application/ignore"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DefaultIgnorePatterns are always excluded when packing, regardless of
|
||||||
|
// whether a .yaoignore file exists. The syntax is identical to .gitignore.
|
||||||
|
var DefaultIgnorePatterns = []string{
|
||||||
|
".git/",
|
||||||
|
".gitignore",
|
||||||
|
".DS_Store",
|
||||||
|
"Thumbs.db",
|
||||||
|
"*.swp",
|
||||||
|
"*.swo",
|
||||||
|
"*.bak",
|
||||||
|
"*.tmp",
|
||||||
|
"*.log",
|
||||||
|
"__debug_bin*",
|
||||||
|
".vscode/",
|
||||||
|
".cursor/",
|
||||||
|
".idea/",
|
||||||
|
"node_modules/",
|
||||||
|
".yaoignore",
|
||||||
|
}
|
||||||
|
|
||||||
// PackDir creates a .yao.zip from a directory. All files under dir are stored
|
// PackDir creates a .yao.zip from a directory. All files under dir are stored
|
||||||
// under the "package/" prefix in the zip. extraFiles maps additional relative
|
// under the "package/" prefix in the zip. extraFiles maps additional relative
|
||||||
// paths (under "package/") to their absolute source paths on disk.
|
// paths (under "package/") to their absolute source paths on disk.
|
||||||
|
|
@ -35,23 +57,32 @@ func PackDir(dir string, manifest *PkgManifest, extraFiles map[string]string) ([
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load ignore rules: built-in defaults first, then .yaoignore on top so
|
||||||
|
// that user negation patterns (e.g. !important.tmp) can override defaults.
|
||||||
|
gi := loadIgnoreRules(filepath.Join(dir, ".yaoignore"))
|
||||||
|
|
||||||
// Walk the main directory
|
// Walk the main directory
|
||||||
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
rel, err := filepath.Rel(dir, path)
|
rel, err := filepath.Rel(dir, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rel = filepath.ToSlash(rel)
|
rel = filepath.ToSlash(rel)
|
||||||
// Skip pkg.yao if it exists in source (we generate our own)
|
if info.IsDir() {
|
||||||
|
if rel != "." && gi.MatchesPath(rel+"/") {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if rel == "pkg.yao" {
|
if rel == "pkg.yao" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if gi.MatchesPath(rel) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return addFileToZip(w, "package/"+rel, path)
|
return addFileToZip(w, "package/"+rel, path)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, fmt.Errorf("walk dir %s: %w", dir, err)
|
return nil, fmt.Errorf("walk dir %s: %w", dir, err)
|
||||||
|
|
@ -191,6 +222,20 @@ func ListZipFiles(zipData []byte) ([]string, error) {
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadIgnoreRules compiles ignore patterns with defaults first, then the
|
||||||
|
// .yaoignore file contents appended so user rules (including negations) win.
|
||||||
|
func loadIgnoreRules(yaoignorePath string) *ignore.GitIgnore {
|
||||||
|
lines := make([]string, 0, len(DefaultIgnorePatterns)+16)
|
||||||
|
lines = append(lines, DefaultIgnorePatterns...)
|
||||||
|
|
||||||
|
if data, err := os.ReadFile(yaoignorePath); err == nil {
|
||||||
|
for _, l := range strings.Split(string(data), "\n") {
|
||||||
|
lines = append(lines, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ignore.CompileIgnoreLines(lines...)
|
||||||
|
}
|
||||||
|
|
||||||
func addFileToZip(w *zip.Writer, zipPath, srcPath string) error {
|
func addFileToZip(w *zip.Writer, zipPath, srcPath string) error {
|
||||||
f, err := w.Create(zipPath)
|
f, err := w.Create(zipPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,132 @@ func TestReadManifestMissing(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPackDirBuiltinIgnore(t *testing.T) {
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "package.yao"), []byte(`{}`), 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "prompts.md"), []byte("hello"), 0644)
|
||||||
|
|
||||||
|
// Files that should be excluded by built-in defaults
|
||||||
|
os.WriteFile(filepath.Join(srcDir, ".DS_Store"), []byte{}, 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "debug.swp"), []byte{}, 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "notes.bak"), []byte{}, 0644)
|
||||||
|
os.MkdirAll(filepath.Join(srcDir, ".git", "objects"), 0755)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, ".git", "config"), []byte{}, 0644)
|
||||||
|
os.MkdirAll(filepath.Join(srcDir, ".vscode"), 0755)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, ".vscode", "settings.json"), []byte{}, 0644)
|
||||||
|
os.MkdirAll(filepath.Join(srcDir, "node_modules", "foo"), 0755)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "node_modules", "foo", "index.js"), []byte{}, 0644)
|
||||||
|
|
||||||
|
manifest := &PkgManifest{Type: TypeAssistant, Scope: "test", Name: "ign", Version: "1.0.0"}
|
||||||
|
zipData, err := PackDir(srcDir, manifest, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PackDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ListZipFiles(zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSet := map[string]bool{}
|
||||||
|
for _, f := range files {
|
||||||
|
fileSet[f] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileSet["package.yao"] {
|
||||||
|
t.Error("expected package.yao in zip")
|
||||||
|
}
|
||||||
|
if !fileSet["prompts.md"] {
|
||||||
|
t.Error("expected prompts.md in zip")
|
||||||
|
}
|
||||||
|
for _, excluded := range []string{".DS_Store", "debug.swp", "notes.bak", ".git/config", ".vscode/settings.json", "node_modules/foo/index.js"} {
|
||||||
|
if fileSet[excluded] {
|
||||||
|
t.Errorf("expected %s to be excluded from zip", excluded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDirYaoignoreFile(t *testing.T) {
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "package.yao"), []byte(`{}`), 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "keep.txt"), []byte("keep"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "secret.key"), []byte("secret"), 0644)
|
||||||
|
os.MkdirAll(filepath.Join(srcDir, "drafts"), 0755)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "drafts", "notes.md"), []byte("draft"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "test.log"), []byte("log"), 0644)
|
||||||
|
|
||||||
|
// .yaoignore excludes *.key and drafts/
|
||||||
|
os.WriteFile(filepath.Join(srcDir, ".yaoignore"), []byte("*.key\ndrafts/\n"), 0644)
|
||||||
|
|
||||||
|
manifest := &PkgManifest{Type: TypeAssistant, Scope: "test", Name: "ign2", Version: "1.0.0"}
|
||||||
|
zipData, err := PackDir(srcDir, manifest, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PackDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ListZipFiles(zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSet := map[string]bool{}
|
||||||
|
for _, f := range files {
|
||||||
|
fileSet[f] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fileSet["package.yao"] {
|
||||||
|
t.Error("expected package.yao")
|
||||||
|
}
|
||||||
|
if !fileSet["keep.txt"] {
|
||||||
|
t.Error("expected keep.txt")
|
||||||
|
}
|
||||||
|
if fileSet["secret.key"] {
|
||||||
|
t.Error("secret.key should be excluded by .yaoignore")
|
||||||
|
}
|
||||||
|
if fileSet["drafts/notes.md"] {
|
||||||
|
t.Error("drafts/notes.md should be excluded by .yaoignore")
|
||||||
|
}
|
||||||
|
if fileSet["test.log"] {
|
||||||
|
t.Error("test.log should be excluded by built-in *.log pattern")
|
||||||
|
}
|
||||||
|
if fileSet[".yaoignore"] {
|
||||||
|
t.Error(".yaoignore itself should be excluded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackDirYaoignoreNegation(t *testing.T) {
|
||||||
|
srcDir := t.TempDir()
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "package.yao"), []byte(`{}`), 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "a.tmp"), []byte("tmp"), 0644)
|
||||||
|
os.WriteFile(filepath.Join(srcDir, "important.tmp"), []byte("keep"), 0644)
|
||||||
|
|
||||||
|
// *.tmp is in defaults, but negate important.tmp
|
||||||
|
os.WriteFile(filepath.Join(srcDir, ".yaoignore"), []byte("!important.tmp\n"), 0644)
|
||||||
|
|
||||||
|
manifest := &PkgManifest{Type: TypeAssistant, Scope: "test", Name: "neg", Version: "1.0.0"}
|
||||||
|
zipData, err := PackDir(srcDir, manifest, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("PackDir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := ListZipFiles(zipData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSet := map[string]bool{}
|
||||||
|
for _, f := range files {
|
||||||
|
fileSet[f] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileSet["a.tmp"] {
|
||||||
|
t.Error("a.tmp should be excluded by built-in *.tmp")
|
||||||
|
}
|
||||||
|
if !fileSet["important.tmp"] {
|
||||||
|
t.Error("important.tmp should be included via negation in .yaoignore")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractFile(t *testing.T) {
|
func TestExtractFile(t *testing.T) {
|
||||||
srcDir := t.TempDir()
|
srcDir := t.TempDir()
|
||||||
os.WriteFile(filepath.Join(srcDir, "data.json"), []byte(`{"key":"value"}`), 0644)
|
os.WriteFile(filepath.Join(srcDir, "data.json"), []byte(`{"key":"value"}`), 0644)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import (
|
||||||
// PushOptions configures the Push operation.
|
// PushOptions configures the Push operation.
|
||||||
type PushOptions struct {
|
type PushOptions struct {
|
||||||
Version string
|
Version string
|
||||||
|
Force bool // delete existing version before push
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push packages and uploads an MCP to the registry.
|
// Push packages and uploads an MCP to the registry.
|
||||||
|
|
@ -72,6 +73,11 @@ func (m *Manager) Push(yaoID string, opts PushOptions) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
regType := common.TypeToRegistryType(common.TypeMCP)
|
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)
|
result, err := m.client.Push(regType, "@"+scope, name, opts.Version, zipData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("push: %w", err)
|
return fmt.Errorf("push: %w", err)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue