// Package app wires discovery, status scanning, and presentation into the // runnable operations used by the CLI commands. package app import ( "context" "fmt" "os" "time" "gitea.oblak.solutions/dimitar/gitFlow/internal/config" "gitea.oblak.solutions/dimitar/gitFlow/internal/scanner" "gitea.oblak.solutions/dimitar/gitFlow/pkg/status" ) // App coordinates the components built from the resolved configuration. type App struct { cfg *config.Config discoverer *scanner.Discoverer scanner *scanner.Scanner warn func(format string, args ...any) } // New builds an App from a validated configuration. func New(cfg *config.Config) (*App, error) { if err := cfg.Validate(); err != nil { return nil, err } return &App{ cfg: cfg, discoverer: scanner.NewDiscoverer( scanner.WithExclude(cfg.Exclude...), scanner.WithMaxDepth(cfg.MaxDepth), ), scanner: scanner.NewScanner(scanner.WithWorkers(cfg.Workers)), warn: func(format string, args ...any) { fmt.Fprintf(os.Stderr, "warning: "+format+"\n", args...) }, }, nil } // ScanOnce discovers repositories under the configured directory and // snapshots their status in a single pass. func (a *App) ScanOnce(ctx context.Context) (status.ScanResult, error) { repos, err := a.discoverer.Discover(ctx, a.cfg.Dir) if err != nil { if len(repos) == 0 { return status.ScanResult{}, err } // Partial discovery (e.g. permission denied on some subtree): // warn and continue with what was found. a.warn("%v", err) } infos, err := a.scanner.Scan(ctx, repos) if err != nil { return status.ScanResult{}, err } return status.ScanResult{ ScannedAt: time.Now(), ParentDir: a.cfg.Dir, Repos: infos, }, nil }