Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 3 additions & 3 deletions detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (d *Detector) sortDefinitions() {
})
}

func (d *Detector) Detect(dir string, opts DetectOptions) (Manager, error) {
func (d *Detector) Detect(dir string, opts DetectOptions) (Manager, error) { //nolint:ireturn
if opts.Manager != "" {
return d.detectExplicit(dir, opts.Manager, opts.RequireCLI)
}
Expand Down Expand Up @@ -100,7 +100,7 @@ func (d *Detector) Detect(dir string, opts DetectOptions) (Manager, error) {
return nil, ErrNoManifest{Dir: dir}
}

func (d *Detector) detectExplicit(dir, managerName string, requireCLI bool) (Manager, error) {
func (d *Detector) detectExplicit(dir, managerName string, requireCLI bool) (Manager, error) { //nolint:ireturn
for _, def := range d.definitions {
if def.Name == managerName {
return d.buildManager(def, dir, nil, requireCLI)
Expand All @@ -109,7 +109,7 @@ func (d *Detector) detectExplicit(dir, managerName string, requireCLI bool) (Man
return nil, ErrNoManifest{Dir: dir}
}

func (d *Detector) buildManager(def *definitions.Definition, dir string, files []string, requireCLI bool) (Manager, error) {
func (d *Detector) buildManager(def *definitions.Definition, dir string, files []string, requireCLI bool) (Manager, error) { //nolint:ireturn
if requireCLI {
if _, err := exec.LookPath(def.Binary); err != nil {
return nil, ErrCLINotFound{
Expand Down
7 changes: 5 additions & 2 deletions docs/examples/dependabot-cron/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import (
)

func main() {
if len(os.Args) < 2 {
const minArgs = 2
if len(os.Args) < minArgs {
fmt.Fprintf(os.Stderr, "Usage: %s <repo-path>\n", os.Args[0])
os.Exit(1)
}
Expand Down Expand Up @@ -269,8 +270,10 @@ type CommandResult struct {
}

// runCommand executes a command and returns the result
const commandTimeout = 5 * time.Minute

func runCommand(ctx context.Context, args []string, dir string) (*CommandResult, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
ctx, cancel := context.WithTimeout(ctx, commandTimeout)
defer cancel()

cmd := exec.CommandContext(ctx, args[0], args[1:]...)
Expand Down
10 changes: 7 additions & 3 deletions docs/examples/git-pkgs-integration/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"github.com/git-pkgs/managers/definitions"
)

const ecosystemNPM = "npm"

// ApplyOptions configures the apply command
type ApplyOptions struct {
RepoPath string
Expand Down Expand Up @@ -183,12 +185,12 @@ func ecosystemToManagerWithFallback(ecosystem, detected string) string {
eco := strings.ToLower(ecosystem)

// For npm ecosystem, use detected manager (npm/pnpm/yarn)
if eco == "npm" {
if eco == ecosystemNPM {
switch detected {
case "npm", "pnpm", "yarn":
return detected
}
return "npm"
return ecosystemNPM
}

// Direct mappings
Expand Down Expand Up @@ -236,8 +238,10 @@ func getGitPkgsOutdated(repoPath, updateType string) ([]OutdatedPackage, error)
return result, nil
}

const commandTimeout = 5 * time.Minute

func executeCommand(ctx context.Context, args []string, dir string) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
ctx, cancel := context.WithTimeout(ctx, commandTimeout)
defer cancel()

cmd := exec.CommandContext(ctx, args[0], args[1:]...)
Expand Down
10 changes: 5 additions & 5 deletions docs/examples/git-pkgs-integration/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/git-pkgs/managers"
"github.com/git-pkgs/managers/definitions"
)

func main() {
if len(os.Args) < 2 {
const minArgs = 2
if len(os.Args) < minArgs {
fmt.Fprintf(os.Stderr, "Usage: %s <repo-path>\n", os.Args[0])
os.Exit(1)
}
Expand Down Expand Up @@ -127,8 +127,8 @@ func applyUpdate(ctx context.Context, tr *managers.Translator, manager, repoPath
}

// runCommand executes a command in the specified directory
func runCommand(ctx context.Context, args []string, dir string) error {
_, cancel := context.WithTimeout(ctx, 5*time.Minute)
func runCommand(ctx context.Context, args []string, _ string) error {
_, cancel := context.WithTimeout(ctx, commandTimeout)
defer cancel()

// In a real implementation, use os/exec
Expand All @@ -142,7 +142,7 @@ func runCommand(ctx context.Context, args []string, dir string) error {
// 1. Import and call git-pkgs/cmd.runOutdated directly
// 2. Execute `git-pkgs outdated --json` and parse output
// 3. Use the same ecosyste.ms client that git-pkgs uses
func getOutdatedFromGitPkgs(repoPath string) []OutdatedPackage {
func getOutdatedFromGitPkgs(_ string) []OutdatedPackage {
// Simulated data - in practice this comes from git-pkgs
return []OutdatedPackage{
{
Expand Down
3 changes: 2 additions & 1 deletion extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ func extractRegex(output string, pattern string) (string, error) {
return "", fmt.Errorf("invalid regex pattern: %w", err)
}

const minSubmatchLen = 2
matches := re.FindStringSubmatch(output)
if len(matches) < 2 {
if len(matches) < minSubmatchLen {
return "", fmt.Errorf("pattern did not match or no capture group found")
}

Expand Down
176 changes: 93 additions & 83 deletions translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,138 +89,148 @@ func (t *Translator) buildCommandChain(binary string, cmd definitions.Command, i
func (t *Translator) buildSingleCommand(binary string, cmd definitions.Command, input CommandInput) ([]string, error) {
args := []string{binary}

// Check for base overrides (e.g., frozen flag changes "install" to "ci" for npm)
baseOverrideUsed := ""
baseOverrideUsed := t.applyBaseOverrides(&args, cmd, input)

packageVal := input.Args["package"]

sortedArgs := t.sortArgs(cmd)

for _, entry := range sortedArgs {
val, err := t.processArg(entry.name, entry.argDef, input, &args)
if err != nil {
return nil, err
}
if val == "" {
continue
}
}

t.applyVersionSuffix(&args, cmd, input, packageVal)

args = append(args, cmd.DefaultFlags...)

t.applyUserFlags(&args, cmd, input, baseOverrideUsed)

args = append(args, input.Extra...)

return args, nil
}

type argEntry struct {
name string
argDef definitions.Arg
}

func (t *Translator) applyBaseOverrides(args *[]string, cmd definitions.Command, input CommandInput) string {
base := cmd.Base
baseOverrideUsed := ""
for flagName, override := range cmd.BaseOverrides {
if val, ok := input.Flags[flagName]; ok && isTruthy(val) {
base = override
baseOverrideUsed = flagName
break
}
}
args = append(args, base...)

// Process args in a deterministic order by position
// First handle package, then version (for suffix handling)
packageVal := ""
if val, ok := input.Args["package"]; ok {
packageVal = val
}
*args = append(*args, base...)
return baseOverrideUsed
}

// Sort args by position to ensure deterministic order
// Flag-style args (with argDef.Flag set) should come after positional args
type argEntry struct {
name string
argDef definitions.Arg
}
var sortedArgs []argEntry
func (t *Translator) sortArgs(cmd definitions.Command) []argEntry {
var sorted []argEntry
for name, argDef := range cmd.Args {
sortedArgs = append(sortedArgs, argEntry{name, argDef})
sorted = append(sorted, argEntry{name, argDef})
}
sort.Slice(sortedArgs, func(i, j int) bool {
// Flag-style args come after positional args
iIsFlag := sortedArgs[i].argDef.Flag != ""
jIsFlag := sortedArgs[j].argDef.Flag != ""
sort.Slice(sorted, func(i, j int) bool {
iIsFlag := sorted[i].argDef.Flag != ""
jIsFlag := sorted[j].argDef.Flag != ""
if iIsFlag != jIsFlag {
return !iIsFlag // positional args (non-flag) come first
return !iIsFlag
}
// Within same category, sort by position
return sortedArgs[i].argDef.Position < sortedArgs[j].argDef.Position
return sorted[i].argDef.Position < sorted[j].argDef.Position
})
return sorted
}

for _, entry := range sortedArgs {
name := entry.name
argDef := entry.argDef
val, provided := input.Args[name]
if !provided {
if argDef.Required && !argDef.ExtractionOnly {
return nil, ErrMissingArgument{Argument: name}
}
continue
func (t *Translator) processArg(name string, argDef definitions.Arg, input CommandInput, args *[]string) (string, error) {
val, provided := input.Args[name]
if !provided {
if argDef.Required && !argDef.ExtractionOnly {
return "", ErrMissingArgument{Argument: name}
}
return "", nil
}

// Skip extraction-only args - they're used for output parsing, not command building
if argDef.ExtractionOnly {
continue
}
if argDef.ExtractionOnly {
return "", nil
}

if argDef.Validate != "" {
if err := t.validate(argDef.Validate, val); err != nil {
return nil, err
}
if argDef.Validate != "" {
if err := t.validate(argDef.Validate, val); err != nil {
return "", err
}
}

if argDef.Flag != "" {
// Flag-style arg: --version "1.0"
args = append(args, argDef.Flag, val)
} else if argDef.FixedSuffix != "" {
// Fixed suffix: package@none
args = append(args, val+argDef.FixedSuffix)
} else if argDef.Suffix != "" && name == "version" {
// Version suffix: find package arg and append @version
// Skip here, handled below
continue
} else {
args = append(args, val)
}
switch {
case argDef.Flag != "":
*args = append(*args, argDef.Flag, val)
case argDef.FixedSuffix != "":
*args = append(*args, val+argDef.FixedSuffix)
case argDef.Suffix != "" && name == "version":
// Handled in applyVersionSuffix
default:
*args = append(*args, val)
}

// Handle version suffix (append to package)
if versionDef, hasVersion := cmd.Args["version"]; hasVersion && versionDef.Suffix != "" {
if version, hasVersionVal := input.Args["version"]; hasVersionVal {
// Find and update the package arg
for i, a := range args {
if a == packageVal {
args[i] = a + versionDef.Suffix + version
break
}
}
return val, nil
}

func (t *Translator) applyVersionSuffix(args *[]string, cmd definitions.Command, input CommandInput, packageVal string) {
versionDef, hasVersion := cmd.Args["version"]
if !hasVersion || versionDef.Suffix == "" {
return
}
version, hasVersionVal := input.Args["version"]
if !hasVersionVal {
return
}
for i, a := range *args {
if a == packageVal {
(*args)[i] = a + versionDef.Suffix + version
break
}
}
}

// Add default flags
args = append(args, cmd.DefaultFlags...)

// Add user-specified flags
func (t *Translator) applyUserFlags(args *[]string, cmd definitions.Command, input CommandInput, baseOverrideUsed string) {
for name, val := range input.Flags {
if val == false || val == "" || val == nil {
continue
}

// Skip flag if it was used for base override
if name == baseOverrideUsed {
continue
}

flagDef, ok := cmd.Flags[name]
if !ok {
continue
}

expanded := t.expandFlag(flagDef, input.Flags)
args = append(args, expanded...)
*args = append(*args, expanded...)
}

// Append any extra raw arguments (escape hatch for manager-specific flags)
args = append(args, input.Extra...)

return args, nil
}

func (t *Translator) expandFlag(flag definitions.Flag, flags map[string]any) []string {
var result []string
for _, v := range flag.Values {
if v.Literal != "" && v.Field != "" && v.Join != "" {
// Joined flag: --group=development
switch {
case v.Literal != "" && v.Field != "" && v.Join != "":
if val, ok := flags[v.Field]; ok {
if s, ok := val.(string); ok && s != "" {
result = append(result, v.Literal+v.Join+s)
}
}
} else if v.Literal != "" {
case v.Literal != "":
result = append(result, v.Literal)
} else if v.Field != "" {
case v.Field != "":
if val, ok := flags[v.Field]; ok {
if s, ok := val.(string); ok && s != "" {
result = append(result, s)
Expand Down
17 changes: 12 additions & 5 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ var defaultValidators = map[string]*regexp.Regexp{
"maven_artifact": regexp.MustCompile(`^[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$`),
}

const (
maxLenDefault = 214
maxLenGem = 128
maxLenCrate = 64
maxLenModule = 256
)

var maxLengths = map[string]int{
"package_name": 214,
"npm_package": 214,
"gem_name": 128,
"cargo_crate": 64,
"go_module": 256,
"package_name": maxLenDefault,
"npm_package": maxLenDefault,
"gem_name": maxLenGem,
"cargo_crate": maxLenCrate,
"go_module": maxLenModule,
}

func ValidatePackageName(validatorName, name string) error {
Expand Down