yao/registry/manager/common/packer.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

206 lines
4.9 KiB
Go

package common
import (
"archive/zip"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// 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
// paths (under "package/") to their absolute source paths on disk.
// The manifest is written as "package/pkg.yao".
func PackDir(dir string, manifest *PkgManifest, extraFiles map[string]string) ([]byte, error) {
var buf bytes.Buffer
w := zip.NewWriter(&buf)
// Sync Dependencies → RawDependencies before serialization
manifest.PrepareMarshal()
// Write pkg.yao manifest
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
return nil, fmt.Errorf("marshal pkg.yao: %w", err)
}
f, err := w.Create("package/pkg.yao")
if err != nil {
return nil, err
}
if _, err := f.Write(data); err != nil {
return nil, err
}
// Walk the main directory
if 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
}
rel = filepath.ToSlash(rel)
// Skip pkg.yao if it exists in source (we generate our own)
if rel == "pkg.yao" {
return nil
}
return addFileToZip(w, "package/"+rel, path)
}); err != nil {
return nil, fmt.Errorf("walk dir %s: %w", dir, err)
}
// Add extra files (e.g., scripts collected from project root)
for relPath, absPath := range extraFiles {
zipPath := "package/" + filepath.ToSlash(relPath)
if err := addFileToZip(w, zipPath, absPath); err != nil {
return nil, fmt.Errorf("add extra file %s: %w", relPath, err)
}
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// UnpackTo extracts the "package/" contents from a .yao.zip to destDir.
// Returns a list of extracted file paths relative to destDir.
func UnpackTo(zipData []byte, destDir string) ([]string, error) {
r, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, fmt.Errorf("open zip: %w", err)
}
var extracted []string
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
name := f.Name
if !strings.HasPrefix(name, "package/") {
continue
}
rel := strings.TrimPrefix(name, "package/")
if rel == "" || rel == "pkg.yao" {
continue
}
dest := filepath.Join(destDir, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
return nil, err
}
rc, err := f.Open()
if err != nil {
return nil, err
}
out, err := os.Create(dest)
if err != nil {
rc.Close()
return nil, err
}
_, copyErr := io.Copy(out, rc)
rc.Close()
out.Close()
if copyErr != nil {
return nil, copyErr
}
extracted = append(extracted, filepath.ToSlash(rel))
}
return extracted, nil
}
// ReadManifest reads and parses the pkg.yao from a .yao.zip byte slice.
func ReadManifest(zipData []byte) (*PkgManifest, error) {
r, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, fmt.Errorf("open zip: %w", err)
}
for _, f := range r.File {
if f.Name == "package/pkg.yao" {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close()
var m PkgManifest
if err := json.NewDecoder(rc).Decode(&m); err != nil {
return nil, fmt.Errorf("decode pkg.yao: %w", err)
}
m.NormalizeDependencies()
return &m, nil
}
}
return nil, fmt.Errorf("pkg.yao not found in zip")
}
// ExtractFile reads a single file from the zip under "package/" prefix.
func ExtractFile(zipData []byte, relPath string) ([]byte, error) {
r, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, fmt.Errorf("open zip: %w", err)
}
target := "package/" + relPath
for _, f := range r.File {
if f.Name == target {
rc, err := f.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return io.ReadAll(rc)
}
}
return nil, fmt.Errorf("file %q not found in zip", relPath)
}
// ListZipFiles returns all file paths in the zip under "package/" prefix,
// excluding "package/pkg.yao".
func ListZipFiles(zipData []byte) ([]string, error) {
r, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, fmt.Errorf("open zip: %w", err)
}
var files []string
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
if !strings.HasPrefix(f.Name, "package/") {
continue
}
rel := strings.TrimPrefix(f.Name, "package/")
if rel == "" || rel == "pkg.yao" {
continue
}
files = append(files, rel)
}
return files, nil
}
func addFileToZip(w *zip.Writer, zipPath, srcPath string) error {
f, err := w.Create(zipPath)
if err != nil {
return err
}
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
_, err = io.Copy(f, src)
return err
}