yao/registry/manager/robot/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

157 lines
4.3 KiB
Go

package robot
import (
"encoding/json"
"fmt"
agentmgr "github.com/yaoapp/yao/registry/manager/agent"
"github.com/yaoapp/yao/registry/manager/common"
mcpmgr "github.com/yaoapp/yao/registry/manager/mcp"
)
// AddOptions configures the Add operation.
type AddOptions struct {
Version string
TeamID string // required: which team to add the robot to
}
// Add installs a robot package from the registry.
// Per DESIGN-ROBOT.md:
// 1. Pull .yao.zip
// 2. Parse robot.json + pkg.yao
// 3. Install dependencies (assistants + MCPs)
// 4. Write member DB record (deferred to CLI layer which has DB access)
// 5. Write registry.yao
func (m *Manager) Add(pkgID string, opts AddOptions) (*RobotJSON, error) {
if opts.Version == "" {
opts.Version = "latest"
}
if opts.TeamID == "" {
return nil, fmt.Errorf("--team is required for robot add")
}
scope, name, err := common.ParsePackageID(pkgID)
if err != nil {
return nil, err
}
lf, err := common.LoadLockfile(m.appRoot)
if err != nil {
return nil, err
}
regType := common.TypeToRegistryType(common.TypeRobot)
zipData, digest, err := m.client.Pull(regType, "@"+scope, name, opts.Version)
if err != nil {
return nil, fmt.Errorf("pull %s: %w", pkgID, err)
}
manifest, err := common.ReadManifest(zipData)
if err != nil {
return nil, fmt.Errorf("read manifest: %w", err)
}
// Read robot.json from zip
robotData, err := common.ExtractFile(zipData, "robot.json")
if err != nil {
return nil, fmt.Errorf("extract robot.json: %w", err)
}
var robot RobotJSON
if err := json.Unmarshal(robotData, &robot); err != nil {
return nil, fmt.Errorf("parse robot.json: %w", err)
}
// Analyze dependencies from robot configuration
analyzedDeps := AnalyzeDeps(&robot)
// Merge with pkg.yao declared dependencies
allDeps := map[string]string{}
for _, dep := range analyzedDeps {
allDeps[dep.PackageID] = "*"
}
for depID, ver := range manifest.Dependencies {
allDeps[depID] = ver
}
// Install dependencies
if len(allDeps) > 0 {
missing, _, _ := common.CheckDependencies(allDeps, lf)
if len(missing) > 0 {
var summary string
for _, dep := range missing {
summary += fmt.Sprintf(" %s %s\n", dep.PackageID, dep.RequiredVersion)
}
if !m.prompter.Confirm(fmt.Sprintf("The following dependencies need to be installed:\n%sInstall?", summary)) {
return nil, fmt.Errorf("dependency installation declined, aborting")
}
for _, dep := range missing {
depScope, depName, err := common.ParsePackageID(dep.PackageID)
if err != nil {
return nil, err
}
// Reload lockfile before each dep install — a previous dep may
// have recursively installed this one already.
freshLF, _ := common.LoadLockfile(m.appRoot)
if _, already := freshLF.GetPackage(dep.PackageID); already {
fmt.Printf(" ✓ Dependency %s already installed (transitive)\n", dep.PackageID)
continue
}
depType := depTypeFor(dep.PackageID, analyzedDeps)
switch depType {
case "mcp":
err = m.mcpMgr.Add(dep.PackageID, mcpmgr.AddOptions{})
default:
err = m.agentMgr.Add(dep.PackageID, agentmgr.AddOptions{})
}
if err != nil {
return nil, fmt.Errorf("failed to install dependency %s (%s/%s): %w", dep.PackageID, depScope, depName, err)
}
fmt.Printf(" ✓ Dependency %s installed\n", dep.PackageID)
}
// Reload lockfile after dependency installation
lf, err = common.LoadLockfile(m.appRoot)
if err != nil {
return nil, err
}
}
}
// Write to registry.yao (member record writing is done by CLI layer)
info := common.PackageInfo{
Type: common.TypeRobot,
Version: manifest.Version,
Integrity: digest,
Dependencies: allDeps,
TeamID: opts.TeamID,
}
lf.SetPackage(pkgID, info)
// Add required_by references
for depID := range allDeps {
lf.AddRequiredBy(depID, pkgID)
}
if err := common.SaveLockfile(m.appRoot, lf); err != nil {
return nil, err
}
fmt.Printf("✓ Robot %s@%s installed (dependencies ready, team: %s)\n", pkgID, manifest.Version, opts.TeamID)
fmt.Printf(" The member record needs to be created in the database.\n")
return &robot, nil
}
// depTypeFor finds the type of a dependency from the analyzed deps list.
func depTypeFor(pkgID string, analyzedDeps []RobotDep) string {
for _, d := range analyzedDeps {
if d.PackageID == pkgID {
return d.Type
}
}
return "assistant"
}