feat/#55 bun support (#56)

* add support for bun package manager

* update readme
This commit is contained in:
Sahil Bansal
2025-07-28 17:23:32 +05:30
committed by GitHub
parent 49b2371cc3
commit c486834c2e
9 changed files with 153 additions and 2 deletions
+18 -1
View File
@@ -63,6 +63,7 @@ pnpm add <package-name>
- [Usage](#usage)
- [Recommended: Automated Setup](#recommended)
- [Alternative: Manual Commands](#alternative)
- [Lockfile Installation](#lockfile)
- [Silent Mode](#silent-mode)
- [Dry Run](#dry-run)
- [Verbose Mode](#verbose-mode)
@@ -87,10 +88,11 @@ PMG supports the following package managers:
| --------------- | --------- | --------------------------- |
| `npm` | ✅ Active | `pmg npm install <package>` |
| `pnpm` | ✅ Active | `pmg pnpm add <package>` |
| `bun` | ✅ Active | `pmg bun add <package>` |
| `pip` | ✅ Active | `pmg pip install <package>` |
| `uv` | 🚧 Planned | |
| `yarn` | 🚧 Planned | |
| `poetry` | 🚧 Planned | |
| `uv` | 🚧 Planned | |
> Want us to support your favorite package manager? [Open an issue](https://github.com/safedep/pmg/issues) and let us know!
@@ -164,6 +166,7 @@ After setup, use your package managers normally:
```bash
npm install <package-name>
pnpm add <package-name>
bun add <package-name>
pip install <package-name>
```
@@ -173,9 +176,23 @@ You can also run PMG manually without aliases:
```bash
pmg npm install <package-name>
pmg pnpm add <package-name>
pmg bun add <package-name>
pmg pip install <package-name>
```
### Lockfile Installation
PMG seamlessly protects lockfile-based installations:
```bash
npm install # Uses package-lock.json
pnpm install # Uses pnpm-lock.yaml
bun install # Uses bun.lock
pip install -r requirements.txt # Uses requirements file
```
PMG scans the exact package versions specified in lockfiles and blocks installation if malicious packages are detected.
### Silent Mode
Use the `--silent` flag to run PMG in silent mode:
+60
View File
@@ -0,0 +1,60 @@
package npm
import (
"context"
"fmt"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/analytics"
"github.com/safedep/pmg/internal/flows"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/packagemanager"
"github.com/spf13/cobra"
)
func NewBunCommand() *cobra.Command {
return &cobra.Command{
Use: "bun [action] [package]",
Short: "Guard bun package manager",
DisableFlagParsing: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := executeBunFlow(cmd.Context(), args)
if err != nil {
log.Errorf("Failed to execute bun flow: %s", err)
}
return nil
},
}
}
func executeBunFlow(ctx context.Context, args []string) error {
analytics.TrackCommandBun()
packageManager, err := packagemanager.NewNpmPackageManager(packagemanager.DefaultBunPackageManagerConfig())
if err != nil {
ui.Fatalf("Failed to create bun package manager proxy: %s", err)
}
config, err := config.FromContext(ctx)
if err != nil {
ui.Fatalf("Failed to get config: %s", err)
}
parsedCommand, err := packageManager.ParseCommand(args)
if err != nil {
return fmt.Errorf("failed to parse command: %w", err)
}
packageResolverConfig := packagemanager.NewDefaultNpmDependencyResolverConfig()
packageResolverConfig.IncludeTransitiveDependencies = config.Transitive
packageResolverConfig.TransitiveDepth = config.TransitiveDepth
packageResolverConfig.IncludeDevDependencies = config.IncludeDevDependencies
packageResolver, err := packagemanager.NewNpmDependencyResolver(packageResolverConfig)
if err != nil {
ui.Fatalf("Failed to create dependency resolver: %s", err)
}
return flows.Common(packageManager, packageResolver, config).Run(ctx, args, parsedCommand)
}
+2
View File
@@ -25,6 +25,7 @@ const (
Npm PackageManagerName = "npm"
Pnpm PackageManagerName = "pnpm"
Pip PackageManagerName = "pip"
Bun PackageManagerName = "bun"
)
type ExtractorManager struct {
@@ -37,6 +38,7 @@ func NewExtractorManager() *ExtractorManager {
Npm: &NpmExtractor{},
Pnpm: &PnpmExtractor{},
Pip: &PipExtractor{},
Bun: &BunExtractor{},
},
}
}
+56
View File
@@ -7,6 +7,7 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/bunlock"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/packagelockjson"
"github.com/google/osv-scalibr/extractor/filesystem/language/javascript/pnpmlock"
"github.com/google/osv-scalibr/fs"
@@ -123,3 +124,58 @@ func parsePnpmLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersio
return packages, nil
}
type BunExtractor struct{}
func (n *BunExtractor) GetSupportedFiles() []string {
return []string{"bun.lock"}
}
func (n *BunExtractor) GetEcosystem() packagev1.Ecosystem {
return packagev1.Ecosystem_ECOSYSTEM_NPM
}
func (n *BunExtractor) GetPackageManager() PackageManagerName {
return Bun
}
func (n *BunExtractor) Extract(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
return parseBunPackageLockFile(lockfilePath, scanDir)
}
func parseBunPackageLockFile(lockfilePath, scanDir string) ([]*packagev1.PackageVersion, error) {
bunlockExtractor := bunlock.New()
file, err := os.Open(lockfilePath)
if err != nil {
return nil, fmt.Errorf("failed to open lockfile: %s", err)
}
defer file.Close()
inputConfig := &filesystem.ScanInput{
FS: fs.DirFS(scanDir),
Path: lockfilePath,
Reader: file,
}
inventory, err := bunlockExtractor.Extract(context.Background(), inputConfig)
if err != nil {
return nil, fmt.Errorf("failed to extract packages: %s", err)
}
var packages []*packagev1.PackageVersion
for _, invPkg := range inventory.Packages {
pkg := &packagev1.PackageVersion{
Package: &packagev1.Package{
Name: invPkg.Name,
Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
},
Version: invPkg.Version,
}
packages = append(packages, pkg)
}
return packages, nil
}
+1
View File
@@ -180,6 +180,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tdakkota/asciicheck v0.4.1 // indirect
github.com/tetafro/godot v1.5.0 // indirect
github.com/tidwall/jsonc v0.3.2 // indirect
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 // indirect
github.com/timonwong/loggercheck v0.10.1 // indirect
github.com/tomarrell/wrapcheck/v2 v2.10.0 // indirect
+2
View File
@@ -448,6 +448,8 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR
github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY=
github.com/tetafro/godot v1.5.0 h1:aNwfVI4I3+gdxjMgYPus9eHmoBeJIbnajOyqZYStzuw=
github.com/tetafro/godot v1.5.0/go.mod h1:2oVxTBSftRTh4+MVfUaUXR6bn2GDXCaMcOG4Dk3rfio=
github.com/tidwall/jsonc v0.3.2 h1:ZTKrmejRlAJYdn0kcaFqRAKlxxFIC21pYq8vLa4p2Wc=
github.com/tidwall/jsonc v0.3.2/go.mod h1:dw+3CIxqHi+t8eFSpzzMlcVYxKp08UP5CD8/uSFCyJE=
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3 h1:y4mJRFlM6fUyPhoXuFg/Yu02fg/nIPFMOY8tOqppoFg=
github.com/timakin/bodyclose v0.0.0-20241017074812-ed6a65f985e3/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460=
github.com/timonwong/loggercheck v0.10.1 h1:uVZYClxQFpw55eh+PIoqM7uAOHMrhVcDoWDery9R8Lg=
+5
View File
@@ -3,6 +3,7 @@ package analytics
const (
eventRun = "pmg_command_run"
eventCommandNpm = "pmg_command_npm"
eventCommandBun = "pmg_command_bun"
eventCommandPnpm = "pmg_command_pnpm"
eventCommandPip = "pmg_command_pip"
@@ -19,6 +20,10 @@ func TrackCommandNpm() {
TrackEvent(eventCommandNpm)
}
func TrackCommandBun() {
TrackEvent(eventCommandBun)
}
func TrackCommandPnpm() {
TrackEvent(eventCommandPnpm)
}
+1
View File
@@ -90,6 +90,7 @@ func main() {
cmd.AddCommand(npm.NewNpmCommand())
cmd.AddCommand(npm.NewPnpmCommand())
cmd.AddCommand(npm.NewBunCommand())
cmd.AddCommand(pypi.NewPipCommand())
cmd.AddCommand(version.NewVersionCommand())
cmd.AddCommand(setup.NewSetupCommand())
+8 -1
View File
@@ -27,6 +27,13 @@ func DefaultPnpmPackageManagerConfig() NpmPackageManagerConfig {
}
}
func DefaultBunPackageManagerConfig() NpmPackageManagerConfig {
return NpmPackageManagerConfig{
InstallCommands: []string{"install", "i", "add"},
CommandName: "bun",
}
}
type npmPackageManager struct {
Config NpmPackageManagerConfig
}
@@ -48,7 +55,7 @@ func (npm *npmPackageManager) Ecosystem() packagev1.Ecosystem {
}
func (npm *npmPackageManager) ParseCommand(args []string) (*ParsedCommand, error) {
if len(args) > 0 && (args[0] == "npm" || args[0] == "pnpm") {
if len(args) > 0 && (args[0] == "npm" || args[0] == "pnpm" || args[0] == "bun") {
args = args[1:]
}