package main import ( "context" "fmt" "os" "github.com/spf13/cobra" "gitea.oblak.solutions/dimitar/gitFlow/internal/ai" "gitea.oblak.solutions/dimitar/gitFlow/internal/app" "gitea.oblak.solutions/dimitar/gitFlow/internal/config" "gitea.oblak.solutions/dimitar/gitFlow/internal/presenter" "gitea.oblak.solutions/dimitar/gitFlow/internal/rules" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) // newScanCmd runs a single scan pass and renders the result. func newScanCmd() *cobra.Command { cmd := &cobra.Command{ Use: "scan", Short: "Scan repositories under a directory once", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { cfg, err := config.Load(cmd.Flags()) if err != nil { return err } // The README asks for the parent directory interactively; // prompt only when nothing was provided and stdin is a TTY. if !cmd.Flags().Changed("dir") && isTerminal(os.Stdin) { if d, err := promptDir(cfg.Dir); err == nil { cfg.Dir = d } } ctx, stop := signalContext() defer stop() a, err := app.New(cfg) if err != nil { return err } result, err := a.ScanOnce(ctx) if err != nil { return err } opts := presenter.Options{ Color: presenter.ParseColorMode(cfg.Color), Theme: presenter.ParseTheme(cfg.Theme), } if err := presenter.Present(os.Stdout, cfg.Format, result, opts); err != nil { return err } // AI suggestions are a human-facing section; they are skipped // in JSON mode so the machine-readable stream stays clean. if cfg.AI.Enabled && cfg.Format != "json" { renderSuggestions(ctx, cfg, opts, result) } if err := renderRuleFlags(opts, cfg, result); err != nil { return err } return nil }, } config.RegisterFlags(cmd.Flags()) return cmd } // renderSuggestions asks the configured AI provider for next actions, // renders them, and — when enabled — runs confirmed commands. Failures are // warnings: a scan result is still useful without AI. func renderSuggestions(ctx context.Context, cfg *config.Config, opts presenter.Options, result status.ScanResult) { provider, err := ai.NewProvider(cfg.AI) if err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err) return } suggestions, err := provider.Suggest(ctx, result) if err != nil { fmt.Fprintf(os.Stderr, "warning: AI suggestions unavailable: %v\n", err) return } if err := presenter.Suggestions(os.Stdout, opts, suggestions); err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err) return } if cfg.AI.Execute && isTerminal(os.Stdin) { if err := ai.RunConfirmed(ctx, suggestions, confirmSuggestion); err != nil { fmt.Fprintf(os.Stderr, "warning: %v\n", err) } } } // confirmSuggestion asks the user to approve running a suggested command. func confirmSuggestion(s ai.Suggestion) (bool, error) { return promptYesNo(false, "Run in %s: $ %s", s.RepoPath, s.Command) } // renderRuleFlags evaluates the configured rules and renders any matches. func renderRuleFlags(opts presenter.Options, cfg *config.Config, result status.ScanResult) error { if len(cfg.Rules) == 0 { return nil } flags, err := rules.Eval(cfg.Rules, result) if err != nil { return err } return presenter.Flags(os.Stdout, opts, flags) } // newConfigCmd prints the effective configuration. func newConfigCmd() *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Show the effective configuration", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { cfg, err := config.Load(cmd.Flags()) if err != nil { return err } out, err := cfg.Dump() if err != nil { return err } fmt.Print(out) return nil }, } config.RegisterFlags(cmd.Flags()) return cmd }