diff --git a/detector.go b/detector.go index be6c1af..c6dd753 100644 --- a/detector.go +++ b/detector.go @@ -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) } @@ -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) @@ -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{ diff --git a/docs/examples/dependabot-cron/main.go b/docs/examples/dependabot-cron/main.go index 201a2b0..0a3ba57 100644 --- a/docs/examples/dependabot-cron/main.go +++ b/docs/examples/dependabot-cron/main.go @@ -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 \n", os.Args[0]) os.Exit(1) } @@ -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:]...) diff --git a/docs/examples/git-pkgs-integration/apply.go b/docs/examples/git-pkgs-integration/apply.go index 2d13390..3cbe7f3 100644 --- a/docs/examples/git-pkgs-integration/apply.go +++ b/docs/examples/git-pkgs-integration/apply.go @@ -19,6 +19,8 @@ import ( "github.com/git-pkgs/managers/definitions" ) +const ecosystemNPM = "npm" + // ApplyOptions configures the apply command type ApplyOptions struct { RepoPath string @@ -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 @@ -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:]...) diff --git a/docs/examples/git-pkgs-integration/main.go b/docs/examples/git-pkgs-integration/main.go index c857bc1..4dd560e 100644 --- a/docs/examples/git-pkgs-integration/main.go +++ b/docs/examples/git-pkgs-integration/main.go @@ -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 \n", os.Args[0]) os.Exit(1) } @@ -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 @@ -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{ { diff --git a/extractor.go b/extractor.go index 4f45bad..b5f27e8 100644 --- a/extractor.go +++ b/extractor.go @@ -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") } diff --git a/translator.go b/translator.go index 7a4bad2..dea178f 100644 --- a/translator.go +++ b/translator.go @@ -89,9 +89,41 @@ 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 @@ -99,128 +131,106 @@ func (t *Translator) buildSingleCommand(binary string, cmd definitions.Command, 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) diff --git a/validate.go b/validate.go index 44abfc9..8d6f069 100644 --- a/validate.go +++ b/validate.go @@ -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 {