feat: Add proxy_install_only config to restrict proxy to download commands

Introduces proxy_install_only (default: false) which, when enabled,
skips the proxy for package manager commands that do not download
packages (e.g. npm ls, pip list), avoiding unnecessary MITM overhead.

- Add ProxyInstallOnly to Config and config template
- Add IsKnownDownloadCommand / MayDownloadPackages to ParsedCommand
- Add DownloadCommands to npm and pypi PM configs covering update,
  ci, audit, dlx, exec, x, download, run and equivalents per PM
- Extract shared runner.Execute used by both proxy flow and guard
- Proxy flow short-circuits to runner.Execute for non-download commands
  when proxy_install_only=true
This commit is contained in:
Sahilb315
2026-04-13 20:15:50 +05:30
parent 887984612c
commit da098a51a8
11 changed files with 387 additions and 69 deletions
+5
View File
@@ -70,6 +70,11 @@ type Config struct {
// we initially introduced it as an experimental feature. // we initially introduced it as an experimental feature.
ExperimentalProxyMode bool `mapstructure:"experimental_proxy_mode"` ExperimentalProxyMode bool `mapstructure:"experimental_proxy_mode"`
// ProxyInstallOnly restricts proxy interception to install commands only.
// When false (default), proxy runs for all package manager commands.
// When true, non-install commands (e.g., npm ls, pip list) bypass the proxy and execute directly.
ProxyInstallOnly bool `mapstructure:"proxy_install_only"`
// Verbosity controls the UI verbosity level. Valid values: "silent", "normal", "verbose". // Verbosity controls the UI verbosity level. Valid values: "silent", "normal", "verbose".
Verbosity string `mapstructure:"verbosity"` Verbosity string `mapstructure:"verbosity"`
+6
View File
@@ -36,6 +36,12 @@ event_log_retention_days: 7
# and can be disabled to fall back to the guard-based analysis. # and can be disabled to fall back to the guard-based analysis.
proxy_mode: true proxy_mode: true
# Restrict proxy to install commands only. Default is false.
# When false, the proxy intercepts all package manager commands (e.g., npm install, npm ls).
# When true, non-install commands bypass the proxy and execute directly, which can improve
# performance for commands that don't download packages (e.g., npm ls, pip list, npm outdated).
proxy_install_only: false
# Trusted packages are packages that are trusted by the user and will be ignored by the security guardrails. # Trusted packages are packages that are trusted by the user and will be ignored by the security guardrails.
# This is useful for packages that are known to be safe and are used in the application. # This is useful for packages that are known to be safe and are used in the application.
# Example: # Example:
+21
View File
@@ -27,6 +27,7 @@ func TestConfigHasDefaultValues(t *testing.T) {
assert.Equal(t, []TrustedPackage{}, config.Config.TrustedPackages) assert.Equal(t, []TrustedPackage{}, config.Config.TrustedPackages)
assert.Equal(t, "/tmp/pmg-test/random-does-not-exist", config.configDir) assert.Equal(t, "/tmp/pmg-test/random-does-not-exist", config.configDir)
assert.Equal(t, "/tmp/pmg-test/random-does-not-exist/config.yml", config.configFilePath) assert.Equal(t, "/tmp/pmg-test/random-does-not-exist/config.yml", config.configFilePath)
assert.Equal(t, false, config.Config.ProxyInstallOnly)
}) })
t.Run("when no config directory is set", func(t *testing.T) { t.Run("when no config directory is set", func(t *testing.T) {
@@ -103,6 +104,26 @@ func TestPartialConfigWithNestedOverride(t *testing.T) {
assert.Equal(t, defaults.ProxyMode, config.Config.ProxyMode) assert.Equal(t, defaults.ProxyMode, config.Config.ProxyMode)
} }
func TestProxyInstallOnlyConfig(t *testing.T) {
t.Run("defaults to false", func(t *testing.T) {
t.Setenv("PMG_CONFIG_DIR", "/tmp/pmg-test/random-does-not-exist")
initConfig()
assert.Equal(t, false, Get().Config.ProxyInstallOnly)
})
t.Run("can be set to true via config file", func(t *testing.T) {
tmpDir := t.TempDir()
t.Setenv("PMG_CONFIG_DIR", tmpDir)
configPath := filepath.Join(tmpDir, "config.yml")
err := os.WriteFile(configPath, []byte("proxy_install_only: true\n"), 0o644)
require.NoError(t, err)
initConfig()
assert.Equal(t, true, Get().Config.ProxyInstallOnly)
})
}
func TestWriteTemplateConfigMergesExistingConfig(t *testing.T) { func TestWriteTemplateConfigMergesExistingConfig(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
t.Setenv("PMG_CONFIG_DIR", tmpDir) t.Setenv("PMG_CONFIG_DIR", tmpDir)
+5 -48
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/exec"
"slices" "slices"
"sync" "sync"
"time" "time"
@@ -16,10 +15,9 @@ import (
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
"github.com/safedep/pmg/extractor" "github.com/safedep/pmg/extractor"
"github.com/safedep/pmg/internal/audit" "github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/internal/runner"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/sandbox/executor"
"github.com/safedep/pmg/usefulerror"
) )
type PackageManagerGuardInteraction struct { type PackageManagerGuardInteraction struct {
@@ -254,52 +252,11 @@ func (g *packageManagerGuard) Run(ctx context.Context, args []string, parsedComm
} }
func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *packagemanager.ParsedCommand) error { func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *packagemanager.ParsedCommand) error {
if len(pc.Command.Exe) == 0 { pmName := ""
return fmt.Errorf("no command to execute") if g.packageManager != nil {
pmName = g.packageManager.Name()
} }
return runner.Execute(ctx, pc, pmName, g.config.DryRun)
if g.config.DryRun {
log.Debugf("Dry run, skipping command execution")
return nil
}
cmd := exec.CommandContext(ctx, pc.Command.Exe, pc.Command.Args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
pmName := g.packageManager.Name()
result, err := executor.ApplySandbox(ctx, cmd, pmName)
if err != nil {
return fmt.Errorf("failed to apply sandbox: %w", err)
}
defer func() {
err := result.Close()
if err != nil {
log.Errorf("failed to close sandbox: %v", err)
}
}()
if result.ShouldRun() {
err := cmd.Run()
if err != nil {
humanError := "Failed to execute package manager command"
if exitErr, ok := err.(*exec.ExitError); ok {
humanError = fmt.Sprintf("Package manager command exited with code: %d", exitErr.ExitCode())
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
WithHumanError(humanError).
WithHelp("Check the package manager command and its arguments").
Wrap(err)
}
return nil
}
return nil
} }
func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context, func (g *packageManagerGuard) concurrentAnalyzePackages(ctx context.Context,
+7
View File
@@ -16,6 +16,7 @@ import (
"github.com/safedep/pmg/guard" "github.com/safedep/pmg/guard"
"github.com/safedep/pmg/internal/audit" "github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/internal/pty" "github.com/safedep/pmg/internal/pty"
"github.com/safedep/pmg/internal/runner"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
@@ -51,6 +52,12 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
cfg := config.Get() cfg := config.Get()
// Skip proxy for commands that don't download packages when proxy_install_only is enabled
if cfg.Config.ProxyInstallOnly && !parsedCmd.MayDownloadPackages() {
log.Debugf("Skipping proxy for non-download command (proxy_install_only=true)")
return runner.Execute(ctx, parsedCmd, f.pm.Name(), cfg.DryRun)
}
// Initialize report data at the start // Initialize report data at the start
reportData := ui.NewReportData() reportData := ui.NewReportData()
reportData.PackageManagerName = f.pm.Name() reportData.PackageManagerName = f.pm.Name()
+59
View File
@@ -0,0 +1,59 @@
package runner
import (
"context"
"fmt"
"os"
"os/exec"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/sandbox/executor"
"github.com/safedep/pmg/usefulerror"
)
// Execute runs a package manager command without proxy or guard analysis.
// It applies sandbox policy if configured, then executes the command directly.
func Execute(ctx context.Context, pc *packagemanager.ParsedCommand, pmName string, dryRun bool) error {
if len(pc.Command.Exe) == 0 {
return fmt.Errorf("no command to execute")
}
if dryRun {
log.Debugf("Dry run, skipping command execution")
return nil
}
cmd := exec.CommandContext(ctx, pc.Command.Exe, pc.Command.Args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
result, err := executor.ApplySandbox(ctx, cmd, pmName)
if err != nil {
return fmt.Errorf("failed to apply sandbox: %w", err)
}
defer func() {
if err := result.Close(); err != nil {
log.Errorf("failed to close sandbox: %v", err)
}
}()
if result.ShouldRun() {
if err := cmd.Run(); err != nil {
humanError := "Failed to execute package manager command"
if exitErr, ok := err.(*exec.ExitError); ok {
humanError = fmt.Sprintf("Package manager command exited with code: %d", exitErr.ExitCode())
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
WithHumanError(humanError).
WithHelp("Check the package manager command and its arguments").
Wrap(err)
}
}
return nil
}
+23 -7
View File
@@ -11,35 +11,45 @@ import (
) )
type NpmPackageManagerConfig struct { type NpmPackageManagerConfig struct {
InstallCommands []string InstallCommands []string
CommandName string DownloadCommands []string
CommandName string
} }
func DefaultNpmPackageManagerConfig() NpmPackageManagerConfig { func DefaultNpmPackageManagerConfig() NpmPackageManagerConfig {
return NpmPackageManagerConfig{ return NpmPackageManagerConfig{
InstallCommands: []string{"install", "i", "add"}, InstallCommands: []string{"install", "i", "add"},
CommandName: "npm", // "exec" is the npm v7+ built-in equivalent of npx.
DownloadCommands: []string{"update", "up", "upgrade", "ci", "audit", "dedupe", "exec"},
CommandName: "npm",
} }
} }
func DefaultPnpmPackageManagerConfig() NpmPackageManagerConfig { func DefaultPnpmPackageManagerConfig() NpmPackageManagerConfig {
return NpmPackageManagerConfig{ return NpmPackageManagerConfig{
InstallCommands: []string{"install", "i", "add"}, InstallCommands: []string{"install", "i", "add"},
CommandName: "pnpm", // "dlx" downloads and runs a package (pnpm's npx equivalent).
// "exec" runs a command from the project's node_modules (may resolve packages).
DownloadCommands: []string{"update", "up", "upgrade", "dedupe", "dlx", "exec"},
CommandName: "pnpm",
} }
} }
func DefaultBunPackageManagerConfig() NpmPackageManagerConfig { func DefaultBunPackageManagerConfig() NpmPackageManagerConfig {
return NpmPackageManagerConfig{ return NpmPackageManagerConfig{
InstallCommands: []string{"install", "i", "add"}, InstallCommands: []string{"install", "i", "add"},
CommandName: "bun", // "x" is bun's npx equivalent (also exposed as the `bunx` binary).
DownloadCommands: []string{"update", "upgrade", "x"},
CommandName: "bun",
} }
} }
func DefaultYarnPackageManagerConfig() NpmPackageManagerConfig { func DefaultYarnPackageManagerConfig() NpmPackageManagerConfig {
return NpmPackageManagerConfig{ return NpmPackageManagerConfig{
InstallCommands: []string{"install", "add", ""}, InstallCommands: []string{"install", "add", ""},
CommandName: "yarn", // "dlx" downloads and runs a package without installing it (yarn's npx equivalent).
DownloadCommands: []string{"upgrade", "up", "dlx"},
CommandName: "yarn",
} }
} }
@@ -96,7 +106,13 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
} }
if installCmdIndex == -1 { if installCmdIndex == -1 {
// No install command found, return as-is // Check if this is a known download command (e.g., npm update, npm ci)
for _, arg := range args {
if slices.Contains(npm.Config.DownloadCommands, arg) {
return &ParsedCommand{Command: command, IsKnownDownloadCommand: true}, nil
}
}
return &ParsedCommand{Command: command}, nil return &ParsedCommand{Command: command}, nil
} }
+130 -2
View File
@@ -71,12 +71,53 @@ func TestNpmParseCommand(t *testing.T) {
}, },
}, },
{ {
name: "not an installation command", name: "update is a known download command",
command: "npm update @types/node", command: "npm update @types/node",
assert: func(t *testing.T, parsedCommand *ParsedCommand, err error) { assert: func(t *testing.T, parsedCommand *ParsedCommand, err error) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, parsedCommand) assert.NotNil(t, parsedCommand)
assert.Equal(t, 0, len(parsedCommand.InstallTargets)) assert.Empty(t, parsedCommand.InstallTargets)
assert.True(t, parsedCommand.IsKnownDownloadCommand)
assert.True(t, parsedCommand.MayDownloadPackages())
},
},
{
name: "ci is a known download command",
command: "npm ci",
assert: func(t *testing.T, parsedCommand *ParsedCommand, err error) {
assert.NoError(t, err)
assert.True(t, parsedCommand.IsKnownDownloadCommand)
assert.True(t, parsedCommand.MayDownloadPackages())
assert.False(t, parsedCommand.IsInstallationCommand())
},
},
{
name: "audit is a known download command",
command: "npm audit",
assert: func(t *testing.T, parsedCommand *ParsedCommand, err error) {
assert.NoError(t, err)
assert.True(t, parsedCommand.IsKnownDownloadCommand)
assert.True(t, parsedCommand.MayDownloadPackages())
},
},
{
name: "ls is not a download command",
command: "npm ls",
assert: func(t *testing.T, parsedCommand *ParsedCommand, err error) {
assert.NoError(t, err)
assert.False(t, parsedCommand.IsKnownDownloadCommand)
assert.False(t, parsedCommand.MayDownloadPackages())
assert.False(t, parsedCommand.IsInstallationCommand())
},
},
{
name: "install sets MayDownloadPackages via IsInstallationCommand",
command: "npm install express",
assert: func(t *testing.T, parsedCommand *ParsedCommand, err error) {
assert.NoError(t, err)
assert.False(t, parsedCommand.IsKnownDownloadCommand)
assert.True(t, parsedCommand.IsInstallationCommand())
assert.True(t, parsedCommand.MayDownloadPackages())
}, },
}, },
{ {
@@ -403,3 +444,90 @@ func TestBunParseCommand(t *testing.T) {
}) })
} }
} }
func TestNpmDownloadCommands(t *testing.T) {
cases := []struct {
name string
pm func() (*npmPackageManager, error)
command string
isKnownDownloadCommand bool
isInstallationCommand bool
}{
{
name: "yarn upgrade is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultYarnPackageManagerConfig()) },
command: "yarn upgrade",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "pnpm update is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultPnpmPackageManagerConfig()) },
command: "pnpm update",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "bun update is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultBunPackageManagerConfig()) },
command: "bun update",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "npm exec is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultNpmPackageManagerConfig()) },
command: "npm exec create-react-app",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "pnpm dlx is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultPnpmPackageManagerConfig()) },
command: "pnpm dlx create-react-app",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "pnpm exec is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultPnpmPackageManagerConfig()) },
command: "pnpm exec tsc",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "yarn dlx is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultYarnPackageManagerConfig()) },
command: "yarn dlx create-react-app",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "bun x is a known download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultBunPackageManagerConfig()) },
command: "bun x create-vite",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "npm outdated is not a download command",
pm: func() (*npmPackageManager, error) { return NewNpmPackageManager(DefaultNpmPackageManagerConfig()) },
command: "npm outdated",
isKnownDownloadCommand: false,
isInstallationCommand: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
pm, err := tc.pm()
assert.NoError(t, err)
parsed, err := pm.ParseCommand(strings.Split(tc.command, " "))
assert.NoError(t, err)
assert.Equal(t, tc.isKnownDownloadCommand, parsed.IsKnownDownloadCommand)
assert.Equal(t, tc.isInstallationCommand, parsed.IsInstallationCommand())
assert.Equal(t, tc.isKnownDownloadCommand || tc.isInstallationCommand, parsed.MayDownloadPackages())
})
}
}
+13
View File
@@ -38,13 +38,26 @@ type ParsedCommand struct {
// ManifestFiles contains the list of manifest files to install from // ManifestFiles contains the list of manifest files to install from
// (e.g., ["requirements.txt"] for pip install -r requirements.txt) // (e.g., ["requirements.txt"] for pip install -r requirements.txt)
ManifestFiles []string ManifestFiles []string
// IsKnownDownloadCommand is true for commands that may download packages but are not
// fully parsed (e.g., npm update, npm ci, poetry update). Used by the proxy to decide
// whether to intercept when proxy_install_only is enabled.
IsKnownDownloadCommand bool
} }
// IsInstallationCommand returns true if command installs packages (explicit targets or from manifest). // IsInstallationCommand returns true if command installs packages (explicit targets or from manifest).
// This is used by guard mode where we need to know which packages are being installed.
func (pc *ParsedCommand) IsInstallationCommand() bool { func (pc *ParsedCommand) IsInstallationCommand() bool {
return pc.HasInstallTarget() || pc.HasManifestInstall() return pc.HasInstallTarget() || pc.HasManifestInstall()
} }
// MayDownloadPackages returns true if the command may download packages from a registry.
// This is broader than IsInstallationCommand and includes commands like npm update or npm ci
// that download packages but are not fully parsed. Used by the proxy to decide interception scope.
func (pc *ParsedCommand) MayDownloadPackages() bool {
return pc.IsInstallationCommand() || pc.IsKnownDownloadCommand
}
func (pc *ParsedCommand) HasInstallTarget() bool { func (pc *ParsedCommand) HasInstallTarget() bool {
return len(pc.InstallTargets) > 0 return len(pc.InstallTargets) > 0
} }
+37 -12
View File
@@ -18,35 +18,42 @@ type pypiCommandParser interface {
} }
type PypiPackageManagerConfig struct { type PypiPackageManagerConfig struct {
InstallCommands []string InstallCommands []string
CommandName string DownloadCommands []string
CommandName string
} }
func DefaultPipPackageManagerConfig() PypiPackageManagerConfig { func DefaultPipPackageManagerConfig() PypiPackageManagerConfig {
return PypiPackageManagerConfig{ return PypiPackageManagerConfig{
InstallCommands: []string{"install"}, InstallCommands: []string{"install"},
CommandName: "pip", DownloadCommands: []string{"download"},
CommandName: "pip",
} }
} }
func DefaultPip3PackageManagerConfig() PypiPackageManagerConfig { func DefaultPip3PackageManagerConfig() PypiPackageManagerConfig {
return PypiPackageManagerConfig{ return PypiPackageManagerConfig{
InstallCommands: []string{"install"}, InstallCommands: []string{"install"},
CommandName: "pip3", DownloadCommands: []string{"download"},
CommandName: "pip3",
} }
} }
func DefaultUvPackageManagerConfig() PypiPackageManagerConfig { func DefaultUvPackageManagerConfig() PypiPackageManagerConfig {
return PypiPackageManagerConfig{ return PypiPackageManagerConfig{
InstallCommands: []string{"add", "install"}, InstallCommands: []string{"add", "install"},
CommandName: "uv", // "download" covers both `uv pip download` and bare `uv download`.
// "run" covers `uv run` which auto-installs script dependencies.
DownloadCommands: []string{"download", "run"},
CommandName: "uv",
} }
} }
func DefaultPoetryPackageManagerConfig() PypiPackageManagerConfig { func DefaultPoetryPackageManagerConfig() PypiPackageManagerConfig {
return PypiPackageManagerConfig{ return PypiPackageManagerConfig{
InstallCommands: []string{"add"}, InstallCommands: []string{"add"},
CommandName: "poetry", DownloadCommands: []string{"update", "install"},
CommandName: "poetry",
} }
} }
@@ -123,7 +130,13 @@ func (p *pipCommandParser) ParseCommand(args []string) (*ParsedCommand, error) {
} }
if installCmdIndex == -1 { if installCmdIndex == -1 {
// No install command found, return as-is // Check if this is a known download command
for _, arg := range args {
if slices.Contains(p.config.DownloadCommands, arg) {
return &ParsedCommand{Command: command, IsKnownDownloadCommand: true}, nil
}
}
return &ParsedCommand{Command: command}, nil return &ParsedCommand{Command: command}, nil
} }
@@ -242,7 +255,13 @@ func (u *uvCommandParser) ParseCommand(args []string) (*ParsedCommand, error) {
} }
if installCmdIndex == -1 { if installCmdIndex == -1 {
// No install command found, return as-is // Check if this is a known download command
for _, arg := range args {
if slices.Contains(u.config.DownloadCommands, arg) {
return &ParsedCommand{Command: command, IsKnownDownloadCommand: true}, nil
}
}
return &ParsedCommand{Command: command}, nil return &ParsedCommand{Command: command}, nil
} }
@@ -340,7 +359,13 @@ func (p *poetryCommandParser) ParseCommand(args []string) (*ParsedCommand, error
} }
if installCmdIndex == -1 { if installCmdIndex == -1 {
// No install command found, return as-is // Check if this is a known download command
for _, arg := range args {
if slices.Contains(p.config.DownloadCommands, arg) {
return &ParsedCommand{Command: command, IsKnownDownloadCommand: true}, nil
}
}
return &ParsedCommand{Command: command}, nil return &ParsedCommand{Command: command}, nil
} }
+81
View File
@@ -1,6 +1,7 @@
package packagemanager package packagemanager
import ( import (
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -856,3 +857,83 @@ func TestPypiConvertWildcardConstraint(t *testing.T) {
}) })
} }
} }
func TestPypiDownloadCommands(t *testing.T) {
cases := []struct {
name string
pm func() (*pypiPackageManager, error)
command string
isKnownDownloadCommand bool
isInstallationCommand bool
}{
{
name: "poetry update is a known download command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultPoetryPackageManagerConfig()) },
command: "poetry update",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "poetry add is an installation command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultPoetryPackageManagerConfig()) },
command: "poetry add django",
isKnownDownloadCommand: false,
isInstallationCommand: true,
},
{
name: "uv sync is an installation command (manifest install)",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultUvPackageManagerConfig()) },
command: "uv sync",
isKnownDownloadCommand: false,
isInstallationCommand: true,
},
{
name: "pip download is a known download command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultPipPackageManagerConfig()) },
command: "pip download requests",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "pip3 download is a known download command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultPip3PackageManagerConfig()) },
command: "pip3 download django",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "uv pip download is a known download command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultUvPackageManagerConfig()) },
command: "uv pip download requests",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "uv run is a known download command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultUvPackageManagerConfig()) },
command: "uv run script.py",
isKnownDownloadCommand: true,
isInstallationCommand: false,
},
{
name: "pip list is not a download command",
pm: func() (*pypiPackageManager, error) { return NewPypiPackageManager(DefaultPipPackageManagerConfig()) },
command: "pip list",
isKnownDownloadCommand: false,
isInstallationCommand: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
pm, err := tc.pm()
assert.NoError(t, err)
parsed, err := pm.ParseCommand(strings.Split(tc.command, " "))
assert.NoError(t, err)
assert.Equal(t, tc.isKnownDownloadCommand, parsed.IsKnownDownloadCommand)
assert.Equal(t, tc.isInstallationCommand, parsed.IsInstallationCommand())
assert.Equal(t, tc.isKnownDownloadCommand || tc.isInstallationCommand, parsed.MayDownloadPackages())
})
}
}