// 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:` 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, ", ") }