chore: README update demo and Error Fix (#126)

* docs: Update README with demo gif

* fix: Proxy remove dependency on interaction

* fix: Update demo gif width

* Update docs/demo/pmg-intro.tape

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>

* fix: PMG demo

---------

Signed-off-by: Abhisek Datta <abhisek.datta@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Abhisek Datta
2026-01-18 16:00:37 +05:30
committed by GitHub
co-authored by Copilot
parent 6a3821d44a
commit edfdd543e0
16 changed files with 163 additions and 38 deletions
+3 -2
View File
@@ -24,11 +24,11 @@ matches the source code they reviewed, eliminating the risk of tampered or malic
## PMG in Action ## PMG in Action
<img src="./docs/assets/pmg-intro.png" width="600" alt="pmg in action"> <img src="./docs/demo/pmg-intro.gif" width="800" alt="pmg in action">
## TL;DR ## TL;DR
Install `pmg` using Homebrew: Install `pmg` using your favorite package manager:
```shell ```shell
# MacOS/Linux with Homebrew # MacOS/Linux with Homebrew
@@ -63,6 +63,7 @@ uv pip install <package-name>
- Malicious package identification using [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) with realtime threat detection - Malicious package identification using [SafeDep Cloud](https://docs.safedep.io/cloud/malware-analysis) with realtime threat detection
- Deep dependency analysis and transitive dependency resolution - Deep dependency analysis and transitive dependency resolution
- Fast and efficient package verification - Fast and efficient package verification
- Defense in depth using OS native sandboxing
- Seamless integration with existing package managers - Seamless integration with existing package managers
- Automated shell integration with cross-shell support - Automated shell integration with cross-shell support
- Package installation tracking and event logging - Package installation tracking and event logging
+3
View File
@@ -0,0 +1,3 @@
package.json
package-lock.json
node_modules
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
# Check if vhs is installed
if ! command -v vhs &> /dev/null; then
echo "vhs could not be found"
exit 1
fi
# Switch to the script directory
cd "$(dirname "$0")"
# Enumerate all .tape files in the current directory
for file in *.tape; do
vhs "$file"
done
Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

+24
View File
@@ -0,0 +1,24 @@
Output pmg-intro.gif
Set FontSize 28
Set Width 1400
Set Height 1000
Set WindowBar Colorful
Type "pmg setup install"
Enter
Sleep 2s
Enter
Type "source ~/.pmg.rc"
Enter
Sleep 2s
Enter
Type "npm install safedep-test-pkg"
Enter
Sleep 5s
Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

+36
View File
@@ -0,0 +1,36 @@
Output pmg-sandbox.gif
Set FontSize 28
Set Width 1400
Set Height 1000
Set WindowBar Colorful
Hide
Type "export CI=true"
Enter
Type "clear"
Enter
Sleep 0.5s
Show
Type "pmg setup install"
Enter
Sleep 2s
Enter
Type "source ~/.pmg.rc"
Enter
Sleep 2s
Enter
Type `npm exec -- \` Enter
Type `node -e "require('child_process').execSync('curl')" \` Enter
Type ` 2>&1 | head -n 8` Enter
Sleep 5s
+16 -1
View File
@@ -19,6 +19,7 @@ import (
"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/sandbox/executor"
"github.com/safedep/pmg/usefulerror"
) )
type PackageManagerGuardInteraction struct { type PackageManagerGuardInteraction struct {
@@ -254,7 +255,21 @@ func (g *packageManagerGuard) continueExecution(ctx context.Context, pc *package
}() }()
if result.ShouldRun() { if result.ShouldRun() {
return cmd.Run() 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 return nil
+27 -5
View File
@@ -55,8 +55,6 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
log.Infof("Dry-run mode: Would execute %s with experimental proxy protection", f.pm.Name()) log.Infof("Dry-run mode: Would execute %s with experimental proxy protection", f.pm.Name())
log.Infof("Dry-run mode: Command would be: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args) log.Infof("Dry-run mode: Command would be: %s %v", parsedCmd.Command.Exe, parsedCmd.Command.Args)
ui.SetStatus("Running in dry-run mode (proxy mode)")
ui.ClearStatus()
return nil return nil
} }
@@ -102,7 +100,7 @@ func (f *proxyFlow) Run(ctx context.Context, args []string, parsedCmd *packagema
SetStatus: ui.SetStatus, SetStatus: ui.SetStatus,
ClearStatus: ui.ClearStatus, ClearStatus: ui.ClearStatus,
ShowWarning: ui.ShowWarning, ShowWarning: ui.ShowWarning,
Block: ui.Block, Block: ui.BlockNoExit,
} }
// Create ecosystem-specific interceptor using factory // Create ecosystem-specific interceptor using factory
@@ -307,7 +305,7 @@ func (f *proxyFlow) executeWithProxyForNonInteractiveTTY(
err = cmd.Run() err = cmd.Run()
if err != nil { if err != nil {
return fmt.Errorf("failed to execute %s: %w", f.pm.Name(), err) return f.handlePackageManagerExecutionError(err)
} }
} }
@@ -454,8 +452,32 @@ func (f *proxyFlow) executeWithProxy(
} }
if sessionError != nil { if sessionError != nil {
return fmt.Errorf("failed to wait for session: %w", sessionError) return f.handlePackageManagerExecutionError(sessionError)
} }
return nil return nil
} }
func (f *proxyFlow) handlePackageManagerExecutionError(err error) error {
if exitErr, ok := err.(*exec.ExitError); ok {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
WithHumanError(fmt.Sprintf("Package manager command exited with code: %d", exitErr.ExitCode())).
WithHelp("Check the package manager command and its arguments").
Wrap(err)
}
if sessionError, ok := err.(*pty.ExitError); ok {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
WithHumanError(fmt.Sprintf("Package manager command exited with code: %d", sessionError.Code)).
WithHelp("Check the package manager command and its arguments").
Wrap(sessionError.Err)
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
WithHumanError("Failed to execute package manager command").
WithHelp("Check the package manager command and its arguments").
Wrap(err)
}
-4
View File
@@ -33,13 +33,9 @@ func ErrorExit(err error) {
} }
// printMinimalError prints error in minimal two-line format: // printMinimalError prints error in minimal two-line format:
// Line 1: Error code (red background) + message (red)
// Line 2: Actionable hint with arrow prefix (dimmed)
func printMinimalError(code, message, hint string) { func printMinimalError(code, message, hint string) {
// Line 1: Error code + message
fmt.Printf("%s %s\n", Colors.ErrorCode(" %s ", code), Colors.Red(message)) fmt.Printf("%s %s\n", Colors.ErrorCode(" %s ", code), Colors.Red(message))
// Line 2: Actionable hint with arrow (only if meaningful)
if hint != "" && hint != "No additional help is available for this error." { if hint != "" && hint != "No additional help is available for this error." {
fmt.Printf(" %s %s\n", Colors.Dim("→"), Colors.Dim(hint)) fmt.Printf(" %s %s\n", Colors.Dim("→"), Colors.Dim(hint))
} }
-1
View File
@@ -76,7 +76,6 @@ var errorMatchers = []errorMatcher{
WithCode(usefulerror.ErrCodeLifecycle). WithCode(usefulerror.ErrCodeLifecycle).
WithHumanError(fmt.Sprintf("Command failed with exit code %d", exitCode)). WithHumanError(fmt.Sprintf("Command failed with exit code %d", exitCode)).
WithHelp("Check command output above"). WithHelp("Check command output above").
WithAdditionalHelp("Run with PMG_DEBUG=true for more details").
Wrap(err) Wrap(err)
}, },
}, },
+12 -1
View File
@@ -54,6 +54,14 @@ func ClearStatus() {
} }
func Block(config *BlockConfig) error { func Block(config *BlockConfig) error {
return blockWithExit(config, true)
}
func BlockNoExit(config *BlockConfig) error {
return blockWithExit(config, false)
}
func blockWithExit(config *BlockConfig, exit bool) error {
StopSpinner() StopSpinner()
fmt.Println() fmt.Println()
@@ -64,7 +72,10 @@ func Block(config *BlockConfig) error {
} }
fmt.Println() fmt.Println()
os.Exit(1)
if exit {
os.Exit(1)
}
return nil return nil
} }
+17 -10
View File
@@ -4,44 +4,51 @@ import (
"github.com/safedep/pmg/usefulerror" "github.com/safedep/pmg/usefulerror"
) )
const (
errDependencyResolutionFailed = "DependencyResolutionFailed"
errPackageParseFailed = "PackageParseFailed"
errPackageAuthorNotFound = "PackageAuthorNotFound"
errGitHubRateLimitExceeded = "GitHubRateLimitExceeded"
)
var ( var (
ErrPackageNotFound = usefulerror.Useful(). ErrPackageNotFound = usefulerror.Useful().
WithCode("package_not_found"). WithCode(usefulerror.ErrCodeNotFound).
WithHumanError("The requested package could not be found."). WithHumanError("The requested package could not be found.").
WithHelp("Please check the package name and try again.") WithHelp("Please check the package name and try again.")
ErrFailedToFetchPackage = usefulerror.Useful(). ErrFailedToFetchPackage = usefulerror.Useful().
WithCode("fetch_failed"). WithCode(usefulerror.ErrCodeNetwork).
WithHumanError("Failed to retrieve the requested package."). WithHumanError("Failed to retrieve the requested package.").
WithHelp("Check your network connection and try again. If the problem persists, the package repository may be temporarily unavailable."). WithHelp("Check your network connection and try again.").
Msg("failed to fetch package") Msg("failed to fetch package")
ErrFailedToResolveVersion = usefulerror.Useful(). ErrFailedToResolveVersion = usefulerror.Useful().
WithCode("resolve_version_failed"). WithCode(usefulerror.ErrCodeNetwork).
WithHumanError("Failed to resolve the requested package version."). WithHumanError("Failed to resolve the requested package version.").
WithHelp("Check your network connection and try again. If the problem persists, the package repository may be temporarily unavailable."). WithHelp("Check your network connection and try again.").
Msg("failed to resolve package version") Msg("failed to resolve package version")
ErrFailedToResolveDependencies = usefulerror.Useful(). ErrFailedToResolveDependencies = usefulerror.Useful().
WithCode("resolve_failed"). WithCode(errDependencyResolutionFailed).
WithHumanError("Failed to resolve dependencies."). WithHumanError("Failed to resolve dependencies.").
WithHelp("Check your network connection and try again. If the problem persists, the package repository may be temporarily unavailable."). WithHelp("Check your network connection and try again.").
Msg("failed to resolve dependencies") Msg("failed to resolve dependencies")
ErrFailedToParsePackage = usefulerror.Useful(). ErrFailedToParsePackage = usefulerror.Useful().
WithCode("parse_failed"). WithCode(errPackageParseFailed).
WithHumanError("The package data could not be processed."). WithHumanError("The package data could not be processed.").
WithHelp("The package may be corrupted or in an unsupported format."). WithHelp("The package may be corrupted or in an unsupported format.").
Msg("failed to parse package") Msg("failed to parse package")
ErrAuthorNotFound = usefulerror.Useful(). ErrAuthorNotFound = usefulerror.Useful().
WithCode("author_not_found"). WithCode(errPackageAuthorNotFound).
WithHumanError("The package author information could not be found."). WithHumanError("The package author information could not be found.").
WithHelp("This may be due to incomplete package metadata or network issues."). WithHelp("This may be due to incomplete package metadata or network issues.").
Msg("author not found") Msg("author not found")
ErrGitHubRateLimitExceeded = usefulerror.Useful(). ErrGitHubRateLimitExceeded = usefulerror.Useful().
WithCode("github_rate_limit"). WithCode(errGitHubRateLimitExceeded).
WithHumanError("GitHub API rate limit has been exceeded."). WithHumanError("GitHub API rate limit has been exceeded.").
WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit."). WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit.").
Msg("github api rate limit exceeded") Msg("github api rate limit exceeded")
-2
View File
@@ -10,7 +10,6 @@ import (
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
"github.com/safedep/pmg/guard"
"github.com/safedep/pmg/internal/eventlog" "github.com/safedep/pmg/internal/eventlog"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
) )
@@ -21,7 +20,6 @@ type baseRegistryInterceptor struct {
analyzer analyzer.PackageVersionAnalyzer analyzer analyzer.PackageVersionAnalyzer
cache AnalysisCache cache AnalysisCache
confirmationChan chan *ConfirmationRequest confirmationChan chan *ConfirmationRequest
interaction guard.PackageManagerGuardInteraction
} }
var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil) var _ proxy.Interceptor = (*baseRegistryInterceptor)(nil)
-3
View File
@@ -8,7 +8,6 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/pmg/analyzer" "github.com/safedep/pmg/analyzer"
"github.com/safedep/pmg/guard"
"github.com/safedep/pmg/proxy" "github.com/safedep/pmg/proxy"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -119,11 +118,9 @@ func TestBaseRegistryInterceptor_HandleAnalysisResult(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
confirmationChan := make(chan *ConfirmationRequest, 1) confirmationChan := make(chan *ConfirmationRequest, 1)
interaction := guard.PackageManagerGuardInteraction{}
base := &baseRegistryInterceptor{ base := &baseRegistryInterceptor{
confirmationChan: confirmationChan, confirmationChan: confirmationChan,
interaction: interaction,
} }
parsedURL, _ := url.Parse("https://registry.npmjs.org/test") parsedURL, _ := url.Parse("https://registry.npmjs.org/test")
+10 -9
View File
@@ -4,13 +4,14 @@ package usefulerror
// We will use a human friendly format for the error codes and not align with posix error codes. // We will use a human friendly format for the error codes and not align with posix error codes.
// Keep this minimal. Reuse first before adding new ones. // Keep this minimal. Reuse first before adding new ones.
const ( const (
ErrCodeInvalidArgument = "InvalidArgument" ErrCodeInvalidArgument = "InvalidArgument"
ErrCodePermissionDenied = "PermissionDenied" ErrCodePermissionDenied = "PermissionDenied"
ErrCodeNotFound = "NotFound" ErrCodeNotFound = "NotFound"
ErrCodeTimeout = "Timeout" ErrCodeTimeout = "Timeout"
ErrCodeCanceled = "Canceled" ErrCodeCanceled = "Canceled"
ErrCodeUnexpectedEOF = "UnexpectedEOF" ErrCodeUnexpectedEOF = "UnexpectedEOF"
ErrCodeUnknown = "Unknown" ErrCodeUnknown = "Unknown"
ErrCodeLifecycle = "Lifecycle" ErrCodeLifecycle = "Lifecycle"
ErrCodeNetwork = "Network" ErrCodeNetwork = "Network"
ErrCodePackageManagerExecutionFailed = "PackageManagerExecutionFailed"
) )