diff --git a/pkg/workflow/package_extraction.go b/pkg/workflow/package_extraction.go index 854fd68fdcd..d67d51dce54 100644 --- a/pkg/workflow/package_extraction.go +++ b/pkg/workflow/package_extraction.go @@ -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"}, @@ -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 ( @@ -27,17 +101,63 @@ 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 "). + // before the package name appears. Set to empty string if the package name + // comes directly after the command. + // + // Examples: + // - "install" for pip (pip install ) + // - "get" for go (go get ) + // - "" for npx (npx ) 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 } @@ -45,24 +165,54 @@ type PackageExtractor struct { // 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")