@@ -21,6 +21,7 @@ import (
2121 "fmt"
2222 log "log/slog"
2323 "os"
24+ "path/filepath"
2425 "strings"
2526
2627 "github.com/spf13/afero"
@@ -99,6 +100,12 @@ func New(options ...Option) (*CLI, error) {
99100 return nil , err
100101 }
101102
103+ // Handle --input-dir flag before building the command tree.
104+ // This must happen before getInfoFromConfigFile() tries to load the PROJECT file.
105+ if err := handleInputDirBeforeConfigLoad (); err != nil {
106+ return nil , err
107+ }
108+
102109 // Build the cmd tree.
103110 if err := c .buildCmd (); err != nil {
104111 c .cmd .RunE = errCmdFunc (err )
@@ -319,6 +326,129 @@ func patchProjectFileInMemoryIfNeeded(fs afero.Fs, path string) error {
319326 return nil
320327}
321328
329+ // parseInputDirFromArgs extracts --input-dir from raw CLI args without depending on
330+ // Cobra flag registration. It supports both:
331+ //
332+ // --input-dir=/path/to/project
333+ // --input-dir /path/to/project
334+ func parseInputDirFromArgs (args []string ) (string , bool ) {
335+ for i := range args {
336+ arg := args [i ]
337+
338+ if arg == "--input-dir" {
339+ if i + 1 >= len (args ) {
340+ // Flag needs an argument but none provided
341+ return "" , false
342+ }
343+ if strings .HasPrefix (args [i + 1 ], "-" ) {
344+ // Next token is another flag, so treat this as missing a value
345+ return "" , false
346+ }
347+ return args [i + 1 ], true
348+ }
349+
350+ if after , ok := strings .CutPrefix (arg , "--input-dir=" ); ok {
351+ return after , true
352+ }
353+ }
354+
355+ return "" , false
356+ }
357+
358+ // consumesNextArgForCommandDetection reports whether a pre-command flag takes its
359+ // value as the next CLI token, so that token should not be treated as a subcommand.
360+ func consumesNextArgForCommandDetection (arg string ) bool {
361+ switch arg {
362+ case "--" + pluginsFlag , "--" + projectVersionFlag :
363+ return true
364+ default :
365+ return false
366+ }
367+ }
368+
369+ // isEditCommand checks if the command invocation is `kubebuilder edit`
370+ // by scanning os.Args for the first positional argument, skipping flags and
371+ // flag values for known pre-command flags.
372+ func isEditCommand (args []string ) bool {
373+ for i := 0 ; i < len (args ); i ++ {
374+ arg := args [i ]
375+
376+ if arg == "--" {
377+ if i + 1 < len (args ) {
378+ return args [i + 1 ] == "edit"
379+ }
380+ return false
381+ }
382+
383+ if strings .HasPrefix (arg , "-" ) {
384+ // Flags using --flag=value consume only the current token.
385+ if strings .Contains (arg , "=" ) {
386+ continue
387+ }
388+
389+ // Skip the next token for known flags that take a separate value.
390+ if consumesNextArgForCommandDetection (arg ) && i + 1 < len (args ) {
391+ i ++
392+ }
393+ continue
394+ }
395+
396+ // First positional argument is the subcommand.
397+ return arg == "edit"
398+ }
399+ return false
400+ }
401+
402+ // handleInputDirBeforeConfigLoad checks if --input-dir flag is set for edit command
403+ // and changes directory if needed. This must run BEFORE the framework loads PROJECT file.
404+ func handleInputDirBeforeConfigLoad () error {
405+ // Only process --input-dir for edit command
406+ if ! isEditCommand (os .Args [1 :]) {
407+ return nil
408+ }
409+
410+ inputDir , found := parseInputDirFromArgs (os .Args [1 :])
411+ if ! found || inputDir == "" {
412+ return nil
413+ }
414+
415+ // Resolve to absolute path
416+ if ! filepath .IsAbs (inputDir ) {
417+ absPath , err := filepath .Abs (inputDir )
418+ if err != nil {
419+ return fmt .Errorf ("failed to resolve input-dir %q: %w" , inputDir , err )
420+ }
421+ inputDir = absPath
422+ }
423+
424+ // Validate directory exists
425+ if info , err := os .Stat (inputDir ); err != nil {
426+ if os .IsNotExist (err ) {
427+ return fmt .Errorf ("input-dir does not exist: %q" , inputDir )
428+ }
429+ return fmt .Errorf ("failed to access input-dir %q: %w" , inputDir , err )
430+ } else if ! info .IsDir () {
431+ return fmt .Errorf ("input-dir is not a directory: %q" , inputDir )
432+ }
433+
434+ // Validate PROJECT file exists in input-dir
435+ projectFilePath := filepath .Join (inputDir , yamlstore .DefaultPath )
436+ if _ , err := os .Stat (projectFilePath ); err != nil {
437+ if os .IsNotExist (err ) {
438+ return fmt .Errorf ("input-dir does not contain %q: %q" , yamlstore .DefaultPath , inputDir )
439+ }
440+ return fmt .Errorf ("failed to access project file %q: %w" , projectFilePath , err )
441+ }
442+
443+ // Change working directory
444+ if err := os .Chdir (inputDir ); err != nil {
445+ return fmt .Errorf ("failed to change to input-dir %q: %w" , inputDir , err )
446+ }
447+
448+ log .Info ("Operating in input directory" , "path" , inputDir )
449+ return nil
450+ }
451+
322452// getInfoFromConfig obtains the project version and plugin keys from the project config.
323453// It is extracted from getInfoFromConfigFile for testing purposes.
324454func (c * CLI ) getInfoFromConfig (projectConfig config.Config ) error {
0 commit comments