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) } } // For both npx and pnpx, the first positional argument is typically // the package to execute (e.g., `npx cowsay@1.6.0` or `pnpx cowsay@1.6.0`). // However, if -p/--package flags are provided, the first positional arg // is the binary to run, not the package (e.g., `npx -p typescript tsc`). if len(flagSet.Args()) > 0 && len(packages) == 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{ IsExplicitVersion: version != "", 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 }