Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 165 additions & 15 deletions pkg/workflow/package_extraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@
//
// # Package Extraction Framework
//
// This file provides a generic framework for extracting package names from command strings.
// The PackageExtractor type can be configured to handle different package managers
// (npm, pip, uv, go, etc.) with minimal code duplication.
// This file provides a reusable, configurable framework for extracting package names
// from command strings. The PackageExtractor type eliminates code duplication by
// providing a single abstraction that handles different package managers (npm, pip,
// uv, go, etc.) with minimal configuration.
//
// # Usage Example
// # Purpose and Benefits
//
// The PackageExtractor pattern exists to:
// - Prevent code duplication across package manager extraction functions
// - Provide a consistent interface for command parsing
// - Centralize package name extraction logic
// - Reduce maintenance burden by having a single implementation
//
// # Usage Pattern
//
// Instead of implementing custom extraction logic, configure a PackageExtractor
// with the appropriate settings for your package manager:
//
// extractor := PackageExtractor{
// CommandNames: []string{"pip", "pip3"},
Expand All @@ -16,8 +28,70 @@
// packages := extractor.ExtractPackages("pip install requests")
// // Returns: []string{"requests"}
//
// For package-specific extraction, see npm.go, pip.go, and dependabot.go.
// For validation, see validation.go.
// # Package Manager Examples
//
// NPM (npx):
//
// extractor := PackageExtractor{
// CommandNames: []string{"npx"},
// RequiredSubcommand: "", // No subcommand needed
// TrimSuffixes: "&|;",
// }
// packages := extractor.ExtractPackages("npx @playwright/mcp@latest")
// // Returns: []string{"@playwright/mcp@latest"}
//
// Python (pip):
//
// extractor := PackageExtractor{
// CommandNames: []string{"pip", "pip3"},
// RequiredSubcommand: "install", // Must have "install" subcommand
// TrimSuffixes: "&|;",
// }
// packages := extractor.ExtractPackages("pip install requests==2.28.0")
// // Returns: []string{"requests==2.28.0"}
//
// Go:
//
// installExtractor := PackageExtractor{
// CommandNames: []string{"go"},
// RequiredSubcommand: "install",
// TrimSuffixes: "&|;",
// }
// packages := installExtractor.ExtractPackages("go install github.com/user/tool@v1.0.0")
// // Returns: []string{"github.com/user/tool@v1.0.0"}
//
// Python (uv):
//
// extractor := PackageExtractor{
// CommandNames: []string{"uvx"},
// RequiredSubcommand: "",
// TrimSuffixes: "&|;",
// }
// packages := extractor.ExtractPackages("uvx black")
// // Returns: []string{"black"}
//
// # Best Practices
//
// - ALWAYS use PackageExtractor instead of reimplementing extraction logic
// - Configure CommandNames for all variations of the command (e.g., ["pip", "pip3"])
// - Set RequiredSubcommand only when the package manager requires it (e.g., "install" for pip)
// - Include common shell operators in TrimSuffixes (typically "&|;")
// - For special cases, use the exported FindPackageName method to reuse logic
//
// # Configuration Details
//
// - CommandNames: List of command names to match (case-sensitive)
// - RequiredSubcommand: Subcommand that must appear before the package name
// (empty string if package comes directly after command)
// - TrimSuffixes: Characters to remove from the end of package names
// (useful for shell operators like "&", "|", ";")
//
// For package-specific extraction implementations, see:
// - npm.go (npx packages)
// - pip.go (pip and uv packages)
// - dependabot.go (go packages)
//
// For package validation, see validation.go.
package workflow

import (
Expand All @@ -27,42 +101,118 @@ import (
// PackageExtractor provides a configurable framework for extracting package names
// from command-line strings. It can be configured to handle different package
// managers (npm, pip, uv, go) by setting the appropriate command names and options.
//
// This type is the core of the package extraction pattern. Use it instead of
// writing custom parsing logic to avoid code duplication.
//
// Configuration:
// - Set CommandNames to all variants of the command (e.g., ["pip", "pip3"])
// - Set RequiredSubcommand if the package manager requires a subcommand
// (e.g., "install" for pip, "get" for go)
// - Set TrimSuffixes to remove shell operators from package names
// (typically "&|;")
//
// Examples:
//
// // For npx (no subcommand):
// extractor := PackageExtractor{
// CommandNames: []string{"npx"},
// RequiredSubcommand: "",
// TrimSuffixes: "&|;",
// }
//
// // For pip (with "install" subcommand):
// extractor := PackageExtractor{
// CommandNames: []string{"pip", "pip3"},
// RequiredSubcommand: "install",
// TrimSuffixes: "&|;",
// }
type PackageExtractor struct {
// CommandNames is the list of command names to look for (e.g., ["pip", "pip3"])
// CommandNames is the list of command names to look for.
// Include all variations of the command (e.g., ["pip", "pip3"]).
// Matching is case-sensitive and exact.
//
// Examples:
// - ["npx"] for npm packages
// - ["pip", "pip3"] for Python packages
// - ["go"] for Go packages
// - ["uvx"] for uv tool packages
CommandNames []string

// RequiredSubcommand is the subcommand that must follow the command name
// (e.g., "install" for pip). If empty, the package name is expected immediately
// after the command name (e.g., "npx <package>").
// before the package name appears. Set to empty string if the package name
// comes directly after the command.
//
// Examples:
// - "install" for pip (pip install <package>)
// - "get" for go (go get <package>)
// - "" for npx (npx <package>)
RequiredSubcommand string

// TrimSuffixes is a string of characters to trim from the end of package names
// (e.g., "&|;" for shell operators)
// TrimSuffixes is a string of characters to trim from the end of package names.
// This is useful for removing shell operators that may appear after package names
// in command strings.
//
// Recommended value: "&|;" (covers common shell operators)
//
// Examples:
// - "pip install requests;" → extracts "requests" (trims ";")
// - "npx playwright&" → extracts "playwright" (trims "&")
TrimSuffixes string
}

// ExtractPackages extracts package names from command strings using the configured
// extraction rules. It processes multi-line command strings and returns all found
// package names.
//
// This is the main entry point for package extraction. Call this method with your
// command string(s) after configuring the PackageExtractor.
//
// The extraction process:
// 1. Split commands by newlines
// 2. Split each line into words
// 3. Find command name matches
// 3. Find command name matches (from CommandNames)
// 4. If RequiredSubcommand is set, look for that subcommand
// 5. Skip flags (words starting with -)
// 6. Extract package name and trim configured suffixes
// 7. Return first package found per command invocation
//
// Example usage:
// Multi-line commands are supported:
//
// commands := `pip install requests
// pip install numpy`
// packages := extractor.ExtractPackages(commands)
// // Returns: []string{"requests", "numpy"}
//
// Flags are automatically skipped:
//
// packages := extractor.ExtractPackages("pip install --upgrade requests")
// // Returns: []string{"requests"}
//
// Shell operators are automatically trimmed:
//
// packages := extractor.ExtractPackages("npx playwright;")
// // Returns: []string{"playwright"}
//
// Example usage with pip:
//
// extractor := PackageExtractor{
// CommandNames: []string{"pip", "pip3"},
// CommandNames: []string{"pip", "pip3"},
// RequiredSubcommand: "install",
// TrimSuffixes: "&|;",
// TrimSuffixes: "&|;",
// }
// packages := extractor.ExtractPackages("pip install requests==2.28.0")
// // Returns: []string{"requests==2.28.0"}
//
// Example usage with npx:
//
// extractor := PackageExtractor{
// CommandNames: []string{"npx"},
// RequiredSubcommand: "",
// TrimSuffixes: "&|;",
// }
// packages := extractor.ExtractPackages("npx @playwright/mcp@latest")
// // Returns: []string{"@playwright/mcp@latest"}
func (pe *PackageExtractor) ExtractPackages(commands string) []string {
var packages []string
lines := strings.Split(commands, "\n")
Expand Down
Loading