package hub import ( "archive/zip" "fmt" "os" "os/exec" "path/filepath" "strings" "time" ) type Team struct { Name string `yaml:"name"` Description string `yaml:"description"` Author string `yaml:"author"` RepoURL string `yaml:"repo_url"` Agents []string `yaml:"agents"` Skills []string `yaml:"skills"` InstalledAt string `yaml:"installed_at"` } // Install clones a git repo (any URL) and installs the team. func Install(repoRef, agentsDir, skillsDir, teamsDir string) (*Team, error) { url := repoRef if !strings.HasPrefix(repoRef, "http") && !strings.HasPrefix(repoRef, "git@") { url = "https://github.com/" + repoRef } // Extract repo name for team name repoName := extractRepoName(url) tmp, err := os.MkdirTemp("", "agent-team-hub-*") if err != nil { return nil, err } defer os.RemoveAll(tmp) cmd := exec.Command("git", "clone", "--depth=1", url, tmp) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return nil, fmt.Errorf("git clone: %w", err) } return installFromDir(tmp, repoName, url, agentsDir, skillsDir, teamsDir) } // InstallZIP extracts a ZIP file and installs the team. func InstallZIP(zipPath, agentsDir, skillsDir, teamsDir string) (*Team, error) { // Extract team name from zip filename teamName := strings.TrimSuffix(filepath.Base(zipPath), ".zip") teamName = strings.TrimSuffix(teamName, "-main") teamName = strings.TrimSuffix(teamName, "-master") tmp, err := os.MkdirTemp("", "agent-team-hub-*") if err != nil { return nil, err } defer os.RemoveAll(tmp) // Extract ZIP if err := extractZIP(zipPath, tmp); err != nil { return nil, fmt.Errorf("extract zip: %w", err) } // Find the root directory (might be wrapped in a subfolder) entries, _ := os.ReadDir(tmp) if len(entries) == 1 && entries[0].IsDir() { tmp = filepath.Join(tmp, entries[0].Name()) } return installFromDir(tmp, teamName, "", agentsDir, skillsDir, teamsDir) } func installFromDir(tmp, teamName, repoURL, agentsDir, skillsDir, teamsDir string) (*Team, error) { // Copy agents installedAgents := []string{} srcAgentsDir := filepath.Join(tmp, "agents") if _, err := os.Stat(srcAgentsDir); err == nil { // Copy to team-specific directory dstAgentsDir := filepath.Join(agentsDir, teamName) if err := copyDir(srcAgentsDir, dstAgentsDir); err == nil { entries, _ := os.ReadDir(srcAgentsDir) for _, e := range entries { if e.IsDir() { installedAgents = append(installedAgents, e.Name()) } } } } // Copy skills installedSkills := []string{} srcSkillsDir := filepath.Join(tmp, "skills") if _, err := os.Stat(srcSkillsDir); err == nil { dstSkillsDir := filepath.Join(skillsDir, teamName) if err := copyDir(srcSkillsDir, dstSkillsDir); err == nil { entries, _ := os.ReadDir(srcSkillsDir) for _, e := range entries { if e.IsDir() { installedSkills = append(installedSkills, e.Name()) } } } } // Create knowledge dir os.MkdirAll(filepath.Join(agentsDir, teamName, "knowledge"), 0755) // Create team record team := &Team{ Name: teamName, Description: "团队描述", RepoURL: repoURL, Agents: installedAgents, Skills: installedSkills, InstalledAt: time.Now().Format("2006-01-02"), } // Save team record teamDir := filepath.Join(teamsDir, teamName) os.MkdirAll(teamDir, 0755) team.save(filepath.Join(teamDir, "team.yaml")) return team, nil } func extractZIP(zipPath, dest string) error { r, err := zip.OpenReader(zipPath) if err != nil { return err } defer r.Close() for _, f := range r.File { fpath := filepath.Join(dest, f.Name) if f.FileInfo().IsDir() { os.MkdirAll(fpath, 0755) continue } if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { return err } outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) if err != nil { return err } rc, err := f.Open() if err != nil { outFile.Close() return err } buf := make([]byte, 4096) for { n, err := rc.Read(buf) if n > 0 { if _, writeErr := outFile.Write(buf[:n]); writeErr != nil { rc.Close() outFile.Close() return writeErr } } if err != nil { break } } rc.Close() outFile.Close() } return nil } func extractRepoName(url string) string { parts := strings.Split(url, "/") name := parts[len(parts)-1] name = strings.TrimSuffix(name, ".git") return name } func (t *Team) save(path string) error { // Simple YAML save content := fmt.Sprintf(`name: %s description: %s author: %s repo_url: %s agents: %s skills: %s installed_at: %s `, t.Name, t.Description, t.Author, t.RepoURL, arrayToYAML(t.Agents), arrayToYAML(t.Skills), t.InstalledAt) return os.WriteFile(path, []byte(content), 0644) } func arrayToYAML(arr []string) string { if len(arr) == 0 { return " []" } result := "" for _, s := range arr { result += fmt.Sprintf(" - %s\n", s) } return result } func copyDir(src, dst string) error { if _, err := os.Stat(src); os.IsNotExist(err) { return nil // optional dir, skip } os.MkdirAll(dst, 0755) entries, err := os.ReadDir(src) if err != nil { return err } for _, e := range entries { srcPath := filepath.Join(src, e.Name()) dstPath := filepath.Join(dst, e.Name()) if e.IsDir() { if err := copyDir(srcPath, dstPath); err != nil { return err } } else { data, err := os.ReadFile(srcPath) if err != nil { return err } os.MkdirAll(filepath.Dir(dstPath), 0755) if err := os.WriteFile(dstPath, data, 0644); err != nil { return err } } } return nil }