package pkg import ( "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) // Manifest is the agent-native.json discovered in a package. type Manifest struct { Name string `json:"name"` Extensions []string `json:"extensions,omitempty"` Config map[string]string `json:"config,omitempty"` } // Manager installs packages under Dir. type Manager struct { Dir string } func (m Manager) root() string { if m.Dir != "" { return m.Dir } home, _ := os.UserHomeDir() return filepath.Join(home, ".agent-native", "packages") } // Install copies a local directory, clones a git URL, or runs npm pack // when src starts with "npm:". The package name comes from its manifest. func (m Manager) Install(src string) (Manifest, error) { dir, err := os.MkdirTemp("", "agent-native-pkg-*") if err != nil { return Manifest{}, err } defer os.RemoveAll(dir) switch { case strings.HasPrefix(src, "npm:"): if _, err := exec.LookPath("npm"); err != nil { return Manifest{}, fmt.Errorf("pkg: npm is not installed") } cmd := exec.Command("npm", "pack", strings.TrimPrefix(src, "npm:")) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { return Manifest{}, fmt.Errorf("pkg: npm pack: %s", out) } case strings.HasPrefix(src, "git+") || strings.HasSuffix(src, ".git") || strings.Contains(src, "://"): cmd := exec.Command("git", "clone", "--depth", "1", src, filepath.Join(dir, "src")) if out, err := cmd.CombinedOutput(); err != nil { return Manifest{}, fmt.Errorf("pkg: git clone: %s", out) } dir = filepath.Join(dir, "src") default: if err := copyDir(src, filepath.Join(dir, "src")); err != nil { return Manifest{}, err } dir = filepath.Join(dir, "src") } manifest, err := ReadManifest(dir) if err != nil { return Manifest{}, err } dest := filepath.Join(m.root(), manifest.Name) if err := os.RemoveAll(dest); err != nil { return Manifest{}, err } if err := copyDir(dir, dest); err != nil { return Manifest{}, err } return manifest, nil } // Remove deletes an installed package. func (m Manager) Remove(name string) error { return os.RemoveAll(filepath.Join(m.root(), name)) } // Update reinstalls from the recorded source when present, otherwise no-ops. func (m Manager) Update(name string) (Manifest, error) { manifest, err := ReadManifest(filepath.Join(m.root(), name)) if err != nil { return Manifest{}, err } src := manifest.Config["source"] if src == "" { return manifest, nil } return m.Install(src) } // List returns installed manifests. func (m Manager) List() ([]Manifest, error) { entries, err := os.ReadDir(m.root()) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var out []Manifest for _, ent := range entries { if !ent.IsDir() { continue } manifest, err := ReadManifest(filepath.Join(m.root(), ent.Name())) if err != nil { continue } out = append(out, manifest) } return out, nil } // Config sets one package config key. func (m Manager) Config(name, key, value string) error { path := filepath.Join(m.root(), name) manifest, err := ReadManifest(path) if err != nil { return err } if manifest.Config == nil { manifest.Config = map[string]string{} } manifest.Config[key] = value raw, err := json.MarshalIndent(manifest, "", " ") if err != nil { return err } return os.WriteFile(filepath.Join(path, "agent-native.json"), raw, 0o644) } // ReadManifest loads agent-native.json from a directory. func ReadManifest(dir string) (Manifest, error) { raw, err := os.ReadFile(filepath.Join(dir, "agent-native.json")) if err != nil { return Manifest{}, err } var manifest Manifest if err := json.Unmarshal(raw, &manifest); err != nil { return Manifest{}, err } if manifest.Name == "" { return Manifest{}, fmt.Errorf("pkg: manifest name is empty") } return manifest, nil } func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } rel, err := filepath.Rel(src, path) if err != nil { return err } target := filepath.Join(dst, rel) if info.IsDir() { return os.MkdirAll(target, 0o755) } if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err } in, err := os.Open(path) if err != nil { return err } defer in.Close() out, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err }) }