yao/registry/manager/agent/add.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

223 lines
6.1 KiB
Go

package agent
import (
"fmt"
"os"
"strings"
"github.com/yaoapp/yao/registry/manager/common"
)
// AddOptions configures the Add operation.
type AddOptions struct {
Version string // version or dist-tag, default "latest"
Force bool // force reinstall even if already installed
}
// Add installs an assistant package from the registry.
// Flow per DESIGN-AGENT.md:
// 1. Parse @scope/name
// 2. Check target path conflict
// 3. Pull from registry
// 4. Check and install dependencies (recursive)
// 5. Unpack to assistants/{scope}/{name}/
// 6. Compute file hashes, write registry.yao
// 7. Hot-reload
func (m *Manager) Add(pkgID string, opts AddOptions) 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
}
// Check if already installed
if existing, ok := lf.GetPackage(pkgID); ok && !opts.Force {
return fmt.Errorf("package %s is already installed (version %s). Use --force to reinstall", pkgID, existing.Version)
}
// Check directory conflict
destDir := common.PackageDir(common.TypeAssistant, scope, name, m.appRoot)
if _, err := os.Stat(destDir); err == nil {
if _, ok := lf.GetPackage(pkgID); !ok {
return fmt.Errorf("directory %s already exists but is not managed by registry. Please remove or relocate it first", destDir)
}
}
// Pull from registry
regType := common.TypeToRegistryType(common.TypeAssistant)
zipData, digest, err := m.client.Pull(regType, "@"+scope, name, opts.Version)
if err != nil {
return fmt.Errorf("pull %s: %w", pkgID, err)
}
// Read manifest
manifest, err := common.ReadManifest(zipData)
if err != nil {
return fmt.Errorf("read manifest: %w", err)
}
// Install dependencies first
if len(manifest.Dependencies) > 0 {
if err := m.installDependencies(manifest.Dependencies, lf, pkgID, map[string]bool{pkgID: true}); err != nil {
return err
}
}
// Unpack to destination
if err := os.MkdirAll(destDir, 0755); err != nil {
return err
}
if _, err := common.UnpackTo(zipData, destDir); err != nil {
return fmt.Errorf("unpack: %w", err)
}
// Compute file hashes
relDir := common.PackageDirRel(common.TypeAssistant, scope, name)
files, err := common.HashDir(destDir, relDir)
if err != nil {
return fmt.Errorf("hash files: %w", err)
}
// Update lockfile
info := common.PackageInfo{
Type: common.TypeAssistant,
Version: manifest.Version,
Integrity: digest,
Dependencies: manifest.Dependencies,
Files: files,
}
lf.SetPackage(pkgID, info)
// Update required_by on dependencies
for depID := range manifest.Dependencies {
lf.AddRequiredBy(depID, pkgID)
}
if err := common.SaveLockfile(m.appRoot, lf); err != nil {
return err
}
// Hot-reload: in production this calls assistant.LoadPath().
// For the manager package we keep it as a no-op since LoadPath requires
// the full engine runtime. The CLI layer will handle hot-reload.
fmt.Printf("✓ Installed %s@%s → %s\n", pkgID, manifest.Version, destDir)
return nil
}
// installDependencies recursively installs missing dependencies.
func (m *Manager) installDependencies(deps map[string]string, lf *common.RegistryYao, parentID string, installing map[string]bool) error {
missing, conflicts, _ := common.CheckDependencies(deps, lf)
// Handle conflicts
for _, c := range conflicts {
msg := fmt.Sprintf(
"⚠ %s is currently %s (required by %s)\n %s requires %s\n",
c.PackageID, c.InstalledVersion, parentID, parentID, c.RequiredVersion,
)
options := []string{
fmt.Sprintf("Upgrade %s (may break other dependents)", c.PackageID),
"Keep current version",
"Abort installation",
}
choice := m.prompter.Choose(msg, options)
switch choice {
case 0:
// Upgrade: treat as missing so it gets reinstalled
missing = append(missing, c)
case 1:
// Keep current
continue
default:
return fmt.Errorf("installation aborted by user")
}
}
if len(missing) == 0 {
return nil
}
// Build summary
var summary strings.Builder
summary.WriteString("The following dependencies need to be installed:\n")
for _, dep := range missing {
summary.WriteString(fmt.Sprintf(" %s %s\n", dep.PackageID, dep.RequiredVersion))
}
if !m.prompter.Confirm(summary.String() + "Install?") {
return fmt.Errorf("dependency installation declined, aborting")
}
for _, dep := range missing {
if common.DetectCycle(installing, dep.PackageID) {
continue
}
installing[dep.PackageID] = true
// Determine type from package ID by trying to pull and reading manifest
depScope, depName, err := common.ParsePackageID(dep.PackageID)
if err != nil {
return err
}
// Try assistant type first, then mcp
var installed bool
for _, regType := range []string{common.TypeDirAssistants, common.TypeDirMCPs} {
zipData, digest, err := m.client.Pull(regType, "@"+depScope, depName, "latest")
if err != nil {
continue
}
manifest, err := common.ReadManifest(zipData)
if err != nil {
continue
}
pkgType := manifest.Type
destDir := common.PackageDir(pkgType, depScope, depName, m.appRoot)
if err := os.MkdirAll(destDir, 0755); err != nil {
return err
}
if _, err := common.UnpackTo(zipData, destDir); err != nil {
return fmt.Errorf("unpack dependency %s: %w", dep.PackageID, err)
}
relDir := common.PackageDirRel(pkgType, depScope, depName)
files, err := common.HashDir(destDir, relDir)
if err != nil {
return err
}
info := common.PackageInfo{
Type: pkgType,
Version: manifest.Version,
Integrity: digest,
Dependencies: manifest.Dependencies,
Files: files,
}
lf.SetPackage(dep.PackageID, info)
// Recursively install this dep's dependencies
if len(manifest.Dependencies) > 0 {
if err := m.installDependencies(manifest.Dependencies, lf, dep.PackageID, installing); err != nil {
return err
}
}
fmt.Printf(" ✓ Dependency %s@%s installed\n", dep.PackageID, manifest.Version)
installed = true
break
}
if !installed {
return fmt.Errorf("failed to install dependency %s: not found in registry", dep.PackageID)
}
}
return nil
}