Files
pmg/packagemanager/npm_executor.go
T
31f23fd065 Add support for package executors and support for PTY handling (#100)
* define contract for package executors

* introduce npx executor

* add npx and pnpx cmd support

* fix typo

* rm PackageExecutor and depend on PackageManager interface

* add support for PTY to handle parent-child process interaction

* refactor PTY handling in proxy flow

* enforce interactiveSession interface check

* close reader explicitly and clean npm version for pkg executors

* rm interaction from interceptors

* add docs and wait for outputRouter before exit

* add support for non interactive TTY for proxy mode

* add support for CI env var check for non interactive tty proxy mode

* update readme to include npx, pnpx support

* Update internal/flows/proxy_flow.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>

* update ptyx lib

* fix docs typo

---------

Signed-off-by: Sahil Bansal <bansalsahil315@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-09 22:03:42 +05:30

124 lines
2.8 KiB
Go

package packagemanager
import (
"io"
"slices"
"strings"
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/spf13/pflag"
)
type NpmPackageExecutorConfig struct {
CommandName string
}
func DefaultNpxPackageExecutorConfig() NpmPackageExecutorConfig {
return NpmPackageExecutorConfig{
CommandName: "npx",
}
}
func DefaultPnpxPackageExecutorConfig() NpmPackageExecutorConfig {
return NpmPackageExecutorConfig{
CommandName: "pnpx",
}
}
type npmPackageExecutor struct {
Config NpmPackageExecutorConfig
}
func NewNpmPackageExecutor(config NpmPackageExecutorConfig) (*npmPackageExecutor, error) {
return &npmPackageExecutor{
Config: config,
}, nil
}
var _ PackageManager = &npmPackageExecutor{}
func (n *npmPackageExecutor) Name() string {
return n.Config.CommandName
}
func (n *npmPackageExecutor) Ecosystem() packagev1.Ecosystem {
return packagev1.Ecosystem_ECOSYSTEM_NPM
}
func (n *npmPackageExecutor) ParseCommand(args []string) (*ParsedCommand, error) {
if len(args) > 0 && (args[0] == "npx" || args[0] == "pnpx") {
args = args[1:]
}
command := Command{Exe: n.Config.CommandName, Args: args}
if len(args) < 1 {
return &ParsedCommand{
Command: command,
}, nil
}
flagSet := pflag.NewFlagSet(n.Config.CommandName, pflag.ContinueOnError)
flagSet.SetOutput(io.Discard)
flagSet.ParseErrorsAllowlist.UnknownFlags = true
var packages []string
switch n.Config.CommandName {
case "npx":
flagSet.StringArrayVarP(&packages, "package", "p", []string{}, "Package List")
case "pnpx":
flagSet.StringArrayVar(&packages, "package", []string{}, "Package List")
}
err := flagSet.Parse(args)
if err != nil {
return &ParsedCommand{Command: command}, nil
}
for _, arg := range flagSet.Args() {
// Append the scoped package
if strings.HasPrefix(arg, "@") && !slices.Contains(packages, arg) {
packages = append(packages, arg)
}
}
// Unlike npx, pnpx does not separate package and binary;
// the first arg is always an install target.
if n.Config.CommandName == "pnpx" && len(flagSet.Args()) > 0 {
pkg := flagSet.Args()[0]
if !slices.Contains(packages, pkg) {
packages = append(packages, pkg)
}
}
var installTargets []*PackageInstallTarget
for _, pkg := range packages {
packageName, version, err := npmParsePackageInfo(pkg)
if err != nil {
return nil, ErrFailedToParsePackage.Wrap(err)
}
if version != "" {
version = npmCleanVersion(version)
}
installTarget := &PackageInstallTarget{
PackageVersion: &packagev1.PackageVersion{
Package: &packagev1.Package{
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
Name: packageName,
},
Version: version,
},
}
installTargets = append(installTargets, installTarget)
}
return &ParsedCommand{
Command: command,
InstallTargets: installTargets,
}, nil
}