feat: add manifest-based package installation detection

This commit is contained in:
Sahilb315
2025-06-10 02:43:56 +05:30
parent 9ed94f5312
commit 7ab669e623
8 changed files with 346 additions and 72 deletions
+23 -5
View File
@@ -51,7 +51,7 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
command := Command{Exe: npm.Config.CommandName, Args: args}
// No command specified
if len(args) < 2 {
if len(args) < 1 {
return &ParsedCommand{
Command: command,
}, nil
@@ -59,8 +59,12 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
// Extract packages from args
var packages []string
var isManifestInstall bool
var foundInstallCmd bool
for idx, arg := range args {
if slices.Contains(npm.Config.InstallCommands, arg) {
foundInstallCmd = true
// All subsequent args are packages except for flags
for i := idx + 1; i < len(args); i++ {
if strings.HasPrefix(args[i], "-") {
@@ -74,8 +78,14 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
}
}
// No packages found
if len(packages) == 0 {
// If install command was found but no explicit packages,
// this is a manifest-based installation (install from package.json)
if foundInstallCmd && len(packages) == 0 {
isManifestInstall = true
}
// No packages found and not a manifest install
if len(packages) == 0 && !isManifestInstall {
return &ParsedCommand{
Command: command,
}, nil
@@ -105,9 +115,17 @@ func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error
})
}
var manifestFiles []string
if isManifestInstall {
// npm/pnpm installs from package.json by default
manifestFiles = append(manifestFiles, "package.json")
}
return &ParsedCommand{
Command: command,
InstallTargets: installTargets,
Command: command,
InstallTargets: installTargets,
IsManifestInstall: isManifestInstall,
ManifestFiles: manifestFiles,
}, nil
}
+87
View File
@@ -121,3 +121,90 @@ func TestNpmParseCommand(t *testing.T) {
})
}
}
func TestNpmParseCommand_ManifestInstallation(t *testing.T) {
pm, err := NewNpmPackageManager(DefaultNpmPackageManagerConfig())
assert.NoError(t, err)
cases := []struct {
name string
args []string
expectedManifest bool
expectedFiles []string
expectedTargets int
}{
{
name: "npm install without args (bare install)",
args: []string{"install"},
expectedManifest: true,
expectedFiles: []string{"package.json"},
expectedTargets: 0,
},
{
name: "npm i without args (short form)",
args: []string{"i"},
expectedManifest: true,
expectedFiles: []string{"package.json"},
expectedTargets: 0,
},
{
name: "npm install with explicit package",
args: []string{"install", "react"},
expectedManifest: false,
expectedFiles: nil,
expectedTargets: 1,
},
{
name: "npm install with multiple packages",
args: []string{"install", "react", "vue"},
expectedManifest: false,
expectedFiles: nil,
expectedTargets: 2,
},
{
name: "npm install with flags but no packages",
args: []string{"install", "--save-dev"},
expectedManifest: true,
expectedFiles: []string{"package.json"},
expectedTargets: 0,
},
{
name: "npm install with mixed args",
args: []string{"install", "react", "--save"},
expectedManifest: false,
expectedFiles: nil,
expectedTargets: 1,
},
{
name: "non-install command",
args: []string{"run", "build"},
expectedManifest: false,
expectedFiles: nil,
expectedTargets: 0,
},
{
name: "pnpm install without args",
args: []string{"install"},
expectedManifest: true,
expectedFiles: []string{"package.json"},
expectedTargets: 0,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
parsed, err := pm.ParseCommand(tc.args)
assert.NoError(t, err)
assert.Equal(t, tc.expectedManifest, parsed.IsManifestInstall, "IsManifestInstall mismatch")
assert.Equal(t, tc.expectedFiles, parsed.ManifestFiles, "ManifestFiles mismatch")
assert.Equal(t, tc.expectedTargets, len(parsed.InstallTargets), "InstallTargets count mismatch")
// Test helper methods
assert.Equal(t, tc.expectedManifest, parsed.HasManifestInstall(), "HasManifestInstall mismatch")
expectedShouldExtract := tc.expectedManifest && tc.expectedTargets == 0
assert.Equal(t, expectedShouldExtract, parsed.ShouldExtractFromManifest(), "ShouldExtractFromManifest mismatch")
})
}
}
+16
View File
@@ -30,12 +30,28 @@ type ParsedCommand struct {
// Parsed install target if this is an install command
InstallTargets []*PackageInstallTarget
// IsManifestInstall indicates if this is a manifest-based installation
// (e.g., npm install, pip install -r requirements.txt)
IsManifestInstall bool
// ManifestFiles contains the list of manifest files to install from
// (e.g., ["requirements.txt"] for pip install -r requirements.txt)
ManifestFiles []string
}
func (pc *ParsedCommand) HasInstallTarget() bool {
return len(pc.InstallTargets) > 0
}
func (pc *ParsedCommand) HasManifestInstall() bool {
return pc.IsManifestInstall
}
func (pc *ParsedCommand) ShouldExtractFromManifest() bool {
return pc.IsManifestInstall && !pc.HasInstallTarget()
}
// PackageManager is the contract for implementing a package manager
type PackageManager interface {
// Name of the package manager implementation
+53 -5
View File
@@ -43,24 +43,70 @@ func (pip *pipPackageManager) ParseCommand(args []string) (*ParsedCommand, error
}
command := Command{Exe: pip.Config.CommandName, Args: args}
if len(args) < 2 {
if len(args) < 1 {
return &ParsedCommand{
Command: command,
}, nil
}
var packages []string
var manifestFiles []string
var isManifestInstall bool
var foundInstallCmd bool
for idx, arg := range args {
if slices.Contains(pip.Config.InstallCommands, arg) {
foundInstallCmd = true
// Check for manifest-based installation flags
for i := idx + 1; i < len(args); i++ {
if strings.HasPrefix(args[i], "-") {
currentArg := args[i]
// Handle -r/--requirement flags
if currentArg == "-r" || currentArg == "--requirement" {
isManifestInstall = true
if i+1 < len(args) {
manifestFiles = append(manifestFiles, args[i+1])
i++ // skip the filename
}
continue
}
packages = append(packages, args[i])
// Handle combined -r flag (e.g., -rrequirements.txt)
if strings.HasPrefix(currentArg, "-r") && len(currentArg) > 2 {
isManifestInstall = true
manifestFiles = append(manifestFiles, currentArg[2:])
continue
}
// Handle other flags that indicate manifest installation
if currentArg == "-e" || currentArg == "--editable" ||
currentArg == "-c" || currentArg == "--constraint" {
if i+1 < len(args) {
i++ // skip the next argument
}
continue
}
// If it's a flag, skip it
if strings.HasPrefix(currentArg, "-") {
continue
}
// Otherwise, it's a package name
packages = append(packages, currentArg)
}
break
}
}
// If install command was found but no explicit packages and no manifest flags,
// check if it's a bare "pip install" (which should look for default manifest files)
if foundInstallCmd && len(packages) == 0 && len(manifestFiles) == 0 {
isManifestInstall = true
// pip install without args typically looks for requirements.txt
manifestFiles = append(manifestFiles, "requirements.txt")
}
var installTargets []*PackageInstallTarget
for _, pkg := range packages {
@@ -96,8 +142,10 @@ func (pip *pipPackageManager) ParseCommand(args []string) (*ParsedCommand, error
}
return &ParsedCommand{
Command: command,
InstallTargets: installTargets,
Command: command,
InstallTargets: installTargets,
IsManifestInstall: isManifestInstall,
ManifestFiles: manifestFiles,
}, nil
}
+87
View File
@@ -111,6 +111,93 @@ func TestPipParsePackageInfo(t *testing.T) {
}
}
func TestPipParseCommand_ManifestInstallation(t *testing.T) {
pm, err := NewPipPackageManager(DefaultPipPackageManagerConfig())
assert.NoError(t, err)
cases := []struct {
name string
args []string
expectedManifest bool
expectedFiles []string
expectedTargets int
}{
{
name: "pip install with -r flag",
args: []string{"install", "-r", "requirements.txt"},
expectedManifest: true,
expectedFiles: []string{"requirements.txt"},
expectedTargets: 0,
},
{
name: "pip install with --requirement flag",
args: []string{"install", "--requirement", "requirements.txt"},
expectedManifest: true,
expectedFiles: []string{"requirements.txt"},
expectedTargets: 0,
},
{
name: "pip install with combined -r flag",
args: []string{"install", "-rrequirements.txt"},
expectedManifest: true,
expectedFiles: []string{"requirements.txt"},
expectedTargets: 0,
},
{
name: "pip install without args (bare install)",
args: []string{"install"},
expectedManifest: true,
expectedFiles: []string{"requirements.txt"},
expectedTargets: 0,
},
{
name: "pip install with explicit package",
args: []string{"install", "django"},
expectedManifest: false,
expectedFiles: nil,
expectedTargets: 1,
},
{
name: "pip install with mixed args",
args: []string{"install", "django", "-r", "requirements.txt"},
expectedManifest: true,
expectedFiles: []string{"requirements.txt"},
expectedTargets: 1,
},
{
name: "pip install with multiple -r flags",
args: []string{"install", "-r", "requirements.txt", "-r", "dev-requirements.txt"},
expectedManifest: true,
expectedFiles: []string{"requirements.txt", "dev-requirements.txt"},
expectedTargets: 0,
},
{
name: "non-install command",
args: []string{"list"},
expectedManifest: false,
expectedFiles: nil,
expectedTargets: 0,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
parsed, err := pm.ParseCommand(tc.args)
assert.NoError(t, err)
assert.Equal(t, tc.expectedManifest, parsed.IsManifestInstall, "IsManifestInstall mismatch")
assert.Equal(t, tc.expectedFiles, parsed.ManifestFiles, "ManifestFiles mismatch")
assert.Equal(t, tc.expectedTargets, len(parsed.InstallTargets), "InstallTargets count mismatch")
// Test helper methods
assert.Equal(t, tc.expectedManifest, parsed.HasManifestInstall(), "HasManifestInstall mismatch")
expectedShouldExtract := tc.expectedManifest && tc.expectedTargets == 0
assert.Equal(t, expectedShouldExtract, parsed.ShouldExtractFromManifest(), "ShouldExtractFromManifest mismatch")
})
}
}
func TestPipConvertCompatibleRelease(t *testing.T) {
cases := []struct {
name string