// Package pkgcmd wires pigo's package-management subcommands (#162, #163, #164) // into the CLI: `pigo install|list|uninstall|update ...`. These are positional // subcommands, distinct from the flag-driven agent modes, so main() peels them // off before pflag parsing (the agent flags don't apply to package management). // // Each subcommand is a thin shell over internal/pkgmgr, which owns all the real // work (fetch, classify, distribute, lockfile). This file only parses argv, // resolves the lockfile path, calls pkgmgr, and prints human-readable results. package pkgcmd import ( "fmt" "io" "github.com/smallnest/pigo/internal/pkgmgr" ) // Subcommands are the argv[1] values routed to Run. var Subcommands = map[string]bool{ "install": true, "list": true, "uninstall": true, "update": true, } // Run executes a package-management subcommand (cmd) with its arguments (args), // writing output to out and errors to errOut. It returns a process exit code. // install is #162; list/uninstall are #163; update is #164. func Run(cmd string, args []string, out, errOut io.Writer) int { lockPath := pkgmgr.DefaultLockfilePath() switch cmd { case "install": return runInstall(args, lockPath, out, errOut) case "list": return runList(lockPath, out, errOut) case "uninstall": return runUninstall(args, lockPath, out, errOut) case "update": return runUpdate(args, lockPath, out, errOut) default: fmt.Fprintf(errOut, "pigo: %q is not yet implemented\n", cmd) return 2 } } // runList handles `pigo list`: it prints one line per installed package // (name, version, types, source), or a friendly notice when none are installed. func runList(lockPath string, out, errOut io.Writer) int { pkgs, err := pkgmgr.ListInstalled(lockPath) if err != nil { fmt.Fprintf(errOut, "pigo: %v\n", err) return 1 } if len(pkgs) == 0 { fmt.Fprintln(out, "no packages installed") return 0 } for _, p := range pkgs { fmt.Fprintf(out, "%s\t%s\t%s\t%s\n", p.Name, p.Version, joinPkgTypes(p.Types), p.Source) } return 0 } // runUninstall handles `pigo uninstall [more...]`. It removes each named // package's files and lockfile entry, continuing past a failure so one bad name // does not abort the rest; the exit code is non-zero if any uninstall failed. func runUninstall(names []string, lockPath string, out, errOut io.Writer) int { if len(names) == 0 { fmt.Fprintln(errOut, "pigo: uninstall requires a package name, e.g. pigo uninstall pi-mcp-adapter") return 2 } failed := false for _, name := range names { if err := pkgmgr.Uninstall(name, lockPath, out); err != nil { fmt.Fprintf(errOut, "pigo: uninstall %s failed: %v\n", name, err) failed = true continue } fmt.Fprintf(out, "Uninstalled %s\n", name) } if failed { return 1 } return 0 } // runInstall handles `pigo install npm:[@version] [more...]`. It requires // npm on PATH (checked once up front for a clear early failure) and installs // each reference in turn, continuing past a failure so one bad package does not // abort the rest; the exit code is non-zero if any install failed. func runInstall(refs []string, lockPath string, out, errOut io.Writer) int { if len(refs) == 0 { fmt.Fprintln(errOut, "pigo: install requires a package reference, e.g. pigo install npm:pi-mcp-adapter") return 2 } if err := pkgmgr.EnsureNPM(); err != nil { fmt.Fprintf(errOut, "pigo: %v\n", err) return 1 } failed := false for _, ref := range refs { res, err := pkgmgr.Install(ref, lockPath, out) if err != nil { fmt.Fprintf(errOut, "pigo: install %s failed: %v\n", ref, err) failed = true continue } fmt.Fprintf(out, "Installed %s@%s (%s) — %d file(s)\n", res.Name, res.Version, joinPkgTypes(res.Types), len(res.Files)) } if failed { return 1 } return 0 } // runUpdate handles `pigo update [more...]`: it updates each named // package, continuing past a failure so one bad package does not abort the rest. // The exit code is non-zero if any update failed. A no-name `pigo update` is // binary self-update, routed away by main() before pkgcmd is reached (US-003), // so here an empty name list is a usage error rather than an update-all. func runUpdate(names []string, lockPath string, out, errOut io.Writer) int { if len(names) == 0 { fmt.Fprintln(errOut, "pigo: update requires a package name, e.g. pigo update pi-mcp-adapter") return 2 } failed := false for _, name := range names { if _, err := pkgmgr.Update(name, lockPath, out); err != nil { fmt.Fprintf(errOut, "pigo: update %s failed: %v\n", name, err) failed = true continue } } if failed { return 1 } return 0 } // joinPkgTypes renders package types as a comma-separated string. func joinPkgTypes(types []pkgmgr.PackageType) string { out := "" for i, t := range types { if i > 0 { out += ", " } out += string(t) } return out }