@@ -21,6 +21,7 @@ import (
2121 _ "github.com/chainguard-dev/omnibump/pkg/languages/golang" // Register Go
2222 _ "github.com/chainguard-dev/omnibump/pkg/languages/java" // Register Java (Maven, Gradle, etc.)
2323 "github.com/chainguard-dev/omnibump/pkg/languages/java/maven"
24+ "github.com/chainguard-dev/omnibump/pkg/languages/js"
2425 _ "github.com/chainguard-dev/omnibump/pkg/languages/php" // Register PHP (Composer, etc.)
2526 _ "github.com/chainguard-dev/omnibump/pkg/languages/rust" // Register Rust
2627 charmlog "github.com/charmbracelet/log"
@@ -30,6 +31,7 @@ import (
3031
3132type rootFlags struct {
3233 language string
34+ managers []string
3335 depsFile string
3436 propertiesFile string
3537 packages string
@@ -77,7 +79,8 @@ func New() *cobra.Command {
7779
7880 // Add root command flags
7981 f := cmd .Flags ()
80- f .StringVarP (& flags .language , "language" , "l" , "auto" , "language to use (auto, java, go, rust, or deprecated: maven)" )
82+ f .StringVarP (& flags .language , "language" , "l" , "auto" , "language to use (auto, java, go, rust, js, or deprecated: maven)" )
83+ f .StringSliceVar (& flags .managers , "manager" , nil , "build tool(s) within a language (currently only used for js: pnpm, yarn, npm, bun). May be repeated or comma-separated to write the same overrides under more than one manager's field." )
8184 f .StringVar (& flags .depsFile , "deps" , "" , "dependencies file (deps.yaml, or legacy names)" )
8285 f .StringVar (& flags .propertiesFile , "properties" , "" , "properties file (properties.yaml)" )
8386 f .StringVar (& flags .packages , "packages" , "" , "inline package list (space-separated)" )
@@ -259,67 +262,33 @@ func runUpdate(cmd *cobra.Command, _ []string) error { // args unused but requir
259262 ctx := cmd .Context ()
260263 log := clog .FromContext (ctx )
261264
262- // Validate input - require at least one input source
263- hasFileInput := flags .depsFile != "" || flags .propertiesFile != ""
264- hasInlineInput := flags .packages != "" || flags .replaces != "" || flags .properties != ""
265-
266- if ! hasFileInput && ! hasInlineInput {
267- return fmt .Errorf ("%w: at least one of --deps, --properties, --packages, --replaces, or --props must be specified" , ErrMissingInput )
268- }
269-
270- if flags .depsFile != "" && flags .packages != "" {
271- return fmt .Errorf ("%w: cannot use both --deps and --packages" , ErrConflictingInput )
272- }
273-
274- if flags .propertiesFile != "" && flags .properties != "" {
275- return fmt .Errorf ("%w: cannot use both --properties (file) and --props (inline)" , ErrConflictingInput )
265+ if err := validateUpdateFlags (); err != nil {
266+ return err
276267 }
277268
278- // Load configuration
279- var cfg * config.Config
280-
281- if hasFileInput {
282- var err error
283- cfg , err = loadFileInputConfig (ctx )
284- if err != nil {
285- return err
286- }
287- } else {
288- var err error
289- cfg , err = loadInlineInputConfig ()
290- if err != nil {
291- return err
292- }
269+ cfg , err := loadUpdateConfig (ctx )
270+ if err != nil {
271+ return err
293272 }
294273
295- // Detect language
296- detectedLang , err := resolveLanguage (ctx , log , cfg )
274+ detectedLang , err := resolveLanguage (ctx , cfg )
297275 if err != nil {
298276 return err
299277 }
300278
301- // Get language implementation
302279 lang , err := languages .Get (detectedLang )
303280 if err != nil {
304281 return fmt .Errorf ("failed to get language implementation: %w" , err )
305282 }
306283
307284 log .Infof ("Using language: %s" , lang .Name ())
308285
309- // Convert config to UpdateConfig
310- updateCfg := convertToUpdateConfig (cfg )
311- updateCfg .RootDir = flags .rootDir
312- updateCfg .Tidy = flags .tidy
313- updateCfg .ShowDiff = flags .showDiff
314- updateCfg .DryRun = flags .dryRun
315- updateCfg .ManifestFile = flags .manifestFile
286+ updateCfg := buildUpdateConfig (cfg )
316287
317- // Perform update
318288 if err := lang .Update (ctx , updateCfg ); err != nil {
319289 return fmt .Errorf ("update failed: %w" , err )
320290 }
321291
322- // Validate
323292 if ! flags .dryRun {
324293 if err := lang .Validate (ctx , updateCfg ); err != nil {
325294 log .Warnf ("Validation completed with warnings: %v" , err )
@@ -330,100 +299,99 @@ func runUpdate(cmd *cobra.Command, _ []string) error { // args unused but requir
330299 return nil
331300}
332301
333- // resolveLanguage determines the target language from flags, manifest detection,
334- // config overrides, and auto-detection — in that priority order.
335- func resolveLanguage ( ctx context. Context , log * clog. Logger , cfg * config. Config ) ( string , error ) {
336- lang := flags .language
302+ // validateUpdateFlags checks the CLI flags for mutually exclusive or missing inputs.
303+ func validateUpdateFlags () error {
304+ hasFileInput := flags . depsFile != "" || flags . propertiesFile != ""
305+ hasInlineInput := flags .packages != "" || flags . replaces != "" || flags . properties != ""
337306
338- // Handle backward compatibility: "maven" -> "java"
339- if lang == languageMaven {
340- log .Warnf ("Language 'maven' is deprecated, use 'java' instead" )
341- lang = languageJava
307+ if ! hasFileInput && ! hasInlineInput {
308+ return fmt .Errorf ("%w: at least one of --deps, --properties, --packages, --replaces, or --props must be specified" , ErrMissingInput )
342309 }
343310
344- // When --manifest is set, detect language from the file content directly.
345- if flags .manifestFile != "" && (lang == languageAuto || lang == "" ) {
311+ if flags .depsFile != "" && flags .packages != "" {
312+ return fmt .Errorf ("%w: cannot use both --deps and --packages" , ErrConflictingInput )
313+ }
314+
315+ if flags .propertiesFile != "" && flags .properties != "" {
316+ return fmt .Errorf ("%w: cannot use both --properties (file) and --props (inline)" , ErrConflictingInput )
317+ }
318+
319+ return nil
320+ }
321+
322+ // loadUpdateConfig loads configuration from file or inline sources based on the flags.
323+ func loadUpdateConfig (ctx context.Context ) (* config.Config , error ) {
324+ if flags .depsFile != "" || flags .propertiesFile != "" {
325+ return loadFileInputConfig (ctx )
326+ }
327+ return loadInlineInputConfig ()
328+ }
329+
330+ // resolveLanguage determines which language implementation to use, honouring
331+ // --language, --manifest, config overrides and auto-detection.
332+ func resolveLanguage (ctx context.Context , cfg * config.Config ) (string , error ) {
333+ log := clog .FromContext (ctx )
334+
335+ detectedLang := normaliseLanguage (flags .language , "Language 'maven' is deprecated, use 'java' instead" , log )
336+
337+ if flags .manifestFile != "" && (detectedLang == languageAuto || detectedLang == "" ) {
346338 ok , err := maven .IsMavenPom (flags .manifestFile )
347339 if err != nil {
348340 return "" , fmt .Errorf ("failed to read manifest file: %w" , err )
349341 }
350342 if ! ok {
351343 return "" , fmt .Errorf ("--manifest %q: %w" , flags .manifestFile , maven .ErrNotMavenPOM )
352344 }
353- lang = languageJava
354- log .Infof ("Detected language: %s" , lang )
345+ detectedLang = languageJava
346+ log .Infof ("Detected language: %s" , detectedLang )
355347 }
356348
357- if lang == languageAuto || lang == "" {
358- detected , err := languages .DetectLanguage (ctx , flags .rootDir )
359- if err != nil && detected == "" {
349+ if detectedLang == languageAuto || detectedLang == "" {
350+ auto , err := languages .DetectLanguage (ctx , flags .rootDir )
351+ if err != nil && auto == "" {
360352 return "" , fmt .Errorf ("failed to detect language: %w (try specifying --language explicitly)" , err )
361353 }
362354 if err != nil {
363355 // Multiple languages detected — warn but proceed with the chosen one.
364356 log .Warnf ("%v" , err )
365357 }
366- lang = detected
367- log .Infof ("Detected language: %s" , lang )
358+ detectedLang = auto
359+ log .Infof ("Detected language: %s" , detectedLang )
368360 }
369361
370- // Override language from config if specified
371- if cfg .Language != "" && cfg .Language != "auto" {
372- lang = cfg .Language
373- // Handle backward compatibility in config too
374- if lang == "maven" {
375- log .Warnf ("Language 'maven' in config is deprecated, use 'java' instead" )
376- lang = "java"
377- }
362+ if cfg .Language != "" && cfg .Language != languageAuto {
363+ detectedLang = normaliseLanguage (cfg .Language , "Language 'maven' in config is deprecated, use 'java' instead" , log )
378364 }
379365
380- return lang , nil
366+ return detectedLang , nil
381367}
382368
383- func convertToUpdateConfig (cfg * config.Config ) * languages.UpdateConfig {
384- updateCfg := & languages.UpdateConfig {
385- Dependencies : make ([]languages.Dependency , 0 , len (cfg .Packages )),
386- Properties : make (map [string ]string ),
387- Options : make (map [string ]any ),
388- }
389-
390- // Convert packages
391- for _ , pkg := range cfg .Packages {
392- dep := languages.Dependency {
393- Name : pkg .Name ,
394- Version : pkg .Version ,
395- Scope : pkg .Scope ,
396- Type : pkg .Type ,
397- Metadata : make (map [string ]any ),
398- }
399-
400- // Store Maven-specific fields in metadata
401- if pkg .GroupID != "" {
402- dep .Metadata ["groupId" ] = pkg .GroupID
403- }
404- if pkg .ArtifactID != "" {
405- dep .Metadata ["artifactId" ] = pkg .ArtifactID
406- }
407-
408- updateCfg .Dependencies = append (updateCfg .Dependencies , dep )
369+ // normaliseLanguage applies backward-compatibility aliasing (e.g. "maven" -> "java").
370+ func normaliseLanguage (name , deprecationMsg string , log * clog.Logger ) string {
371+ if name == languageMaven {
372+ log .Warnf ("%s" , deprecationMsg )
373+ return languageJava
409374 }
375+ return name
376+ }
410377
411- // Convert properties
412- for _ , prop := range cfg .Properties {
413- updateCfg .Properties [prop .Property ] = prop .Value
414- }
378+ // buildUpdateConfig converts the loaded config plus CLI flags into an UpdateConfig.
379+ func buildUpdateConfig (cfg * config.Config ) * languages.UpdateConfig {
380+ updateCfg := cfg .ToUpdateConfig ()
381+ updateCfg .RootDir = flags .rootDir
382+ updateCfg .Tidy = flags .tidy
383+ updateCfg .ShowDiff = flags .showDiff
384+ updateCfg .DryRun = flags .dryRun
385+ updateCfg .ManifestFile = flags .manifestFile
415386
416- // Convert replaces (Go-specific)
417- if len (cfg .Replaces ) > 0 {
418- for _ , repl := range cfg .Replaces {
419- dep := languages.Dependency {
420- OldName : repl .OldName ,
421- Name : repl .Name ,
422- Version : repl .Version ,
423- Replace : true ,
424- }
425- updateCfg .Dependencies = append (updateCfg .Dependencies , dep )
387+ // The CLI's --manager flag always wins over a value in the deps file
388+ // (which ToUpdateConfig already stamped into Options).
389+ if len (flags .managers ) > 0 {
390+ managers := make ([]js.Manager , len (flags .managers ))
391+ for i , s := range flags .managers {
392+ managers [i ] = js .Manager (s )
426393 }
394+ updateCfg .Options ["manager" ] = managers
427395 }
428396
429397 return updateCfg
0 commit comments