first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+122
View File
@@ -0,0 +1,122 @@
// This file is the install orchestrator (#162): it ties together the pieces the
// earlier issues built — fetch (#156), classify (#157), the per-type
// distributors (#158-#161), and the lockfile (#154) — into the single
// `pigo install npm:<name>` flow.
//
// The flow is: parse the reference, fetch+extract the package to a temp dir,
// classify it into one or more pi types, distribute each type to its target
// directory, then record everything laid down in the lockfile so list/uninstall
// /update can act on it. The temp dir is always cleaned up.
package pkgmgr
import (
"fmt"
"io"
"sort"
"strings"
)
// InstallResult reports what an install did, for the CLI to print.
type InstallResult struct {
Name string
Version string
Types []PackageType
// Files is every path laid down on disk across all distributed types.
Files []string
}
// Install fetches, classifies, and distributes the package named by rawRef
// (e.g. "npm:pi-mcp-adapter"), then records it in the lockfile at lockfilePath.
// Progress is written to logw when non-nil. It returns a summary of the install.
//
// The install is type-driven: a package classified as several types (e.g.
// extension+skill) is distributed to each corresponding directory. If any
// distribution step fails, the error is returned; already-written files are left
// for the caller/uninstall to reconcile via a re-run (distribution is
// idempotent — each distributor clears its own stale target first).
func Install(rawRef, lockfilePath string, logw io.Writer) (InstallResult, error) {
logf := func(format string, a ...any) {
if logw != nil {
fmt.Fprintf(logw, format, a...)
}
}
ref, err := ParsePackageRef(rawRef)
if err != nil {
return InstallResult{}, err
}
logf("Fetching %s ...\n", ref.String())
fetched, err := Fetch(ref)
if err != nil {
return InstallResult{}, err
}
defer fetched.Cleanup()
return installFetched(fetched.Dir, ref, lockfilePath, logf)
}
// installFetched runs the post-fetch half of an install: classify the already
// extracted package at pkgDir, distribute each type, and record the result in
// the lockfile. It is shared by Install (#162) and Update (#164) so both go
// through the same classify→distribute→lockfile path.
func installFetched(pkgDir string, ref PackageRef, lockfilePath string, logf func(string, ...any)) (InstallResult, error) {
name, version, types, err := Classify(pkgDir)
if err != nil {
return InstallResult{}, err
}
logf("Installing %s@%s (%s)\n", name, version, joinTypes(types))
var files []string
for _, t := range types {
created, derr := distribute(t, pkgDir, name)
if derr != nil {
return InstallResult{}, derr
}
files = append(files, created...)
}
sort.Strings(files)
lf, err := Load(lockfilePath)
if err != nil {
return InstallResult{}, err
}
lf.Set(InstalledPackage{
Name: name,
Source: ref.String(),
Version: version,
Types: types,
Files: files,
})
if err := lf.Save(); err != nil {
return InstallResult{}, err
}
return InstallResult{Name: name, Version: version, Types: types, Files: files}, nil
}
// distribute routes one package type to its distributor. An unknown type is an
// error (Classify should never produce one, but guard anyway).
func distribute(t PackageType, pkgDir, name string) ([]string, error) {
switch t {
case TypeExtension:
return DistributeExtension(pkgDir, name)
case TypeSkill:
return DistributeSkill(pkgDir, name)
case TypePrompt:
return DistributePrompt(pkgDir, name)
case TypeTheme:
return DistributeTheme(pkgDir, name)
default:
return nil, fmt.Errorf("pkgmgr: cannot distribute unknown type %q", t)
}
}
// joinTypes renders a type slice as a comma-separated string for logging.
func joinTypes(types []PackageType) string {
parts := make([]string, len(types))
for i, t := range types {
parts[i] = string(t)
}
return strings.Join(parts, ", ")
}