22package app
33
44import (
5+ "errors"
56 "fmt"
67 "os"
78 "os/exec"
@@ -23,6 +24,7 @@ type Config struct {
2324 Author , User , Repos , Root string
2425 Out , Format string
2526 NoEdit , All , Copy , NoPR bool
27+ Setup bool // force the guided first-run setup
2628}
2729
2830// Run executes the whole pipeline.
@@ -31,7 +33,31 @@ func Run(c Config) error {
3133 return fmt .Errorf ("git is required but was not found on PATH" )
3234 }
3335
34- repos , err := config .ResolveRepos (splitCSV (c .Repos ), splitCSV (c .Root ))
36+ interactive := isTerminal ()
37+
38+ // --setup forces the guided flow regardless of existing config.
39+ if c .Setup && ! interactive {
40+ return fmt .Errorf ("--setup needs an interactive terminal" )
41+ }
42+
43+ ranSetup := false
44+ var repos []string
45+ var err error
46+ if c .Setup {
47+ repos , err = firstRunSetup ()
48+ ranSetup = true
49+ } else {
50+ repos , err = config .ResolveRepos (splitCSV (c .Repos ), splitCSV (c .Root ))
51+ if errors .Is (err , config .ErrNoRepos ) {
52+ if ! interactive {
53+ return fmt .Errorf ("no repositories configured.\n " +
54+ " Point at repos with --repos <path,…> or --root <dir>, or configure\n " +
55+ " ~/.config/commit-chronicle/{repos,roots}. Run in a terminal for guided setup." )
56+ }
57+ repos , err = firstRunSetup ()
58+ ranSetup = true
59+ }
60+ }
3561 if err != nil {
3662 return err
3763 }
@@ -44,8 +70,6 @@ func Run(c Config) error {
4470 return fmt .Errorf ("could not determine author; pass --author \" Your Name\" " )
4571 }
4672
47- interactive := isTerminal ()
48-
4973 rng , err := resolveRange (c , interactive )
5074 if err != nil {
5175 return err
@@ -56,6 +80,15 @@ func Run(c Config) error {
5680 if ghUser == "" && ! c .NoPR && collect .HasGH () {
5781 ghUser = ghLogin ()
5882 }
83+ // If PRs/reviews were wanted but gh can't provide them, say so once (setup
84+ // already reports gh status, so skip the note right after first-run setup).
85+ if ! c .NoPR && ! ranSetup && ghUser == "" {
86+ if ! collect .HasGH () {
87+ fmt .Fprintln (os .Stderr , "ℹ️ gh not found — commits only (install gh + `gh auth login` for PRs & reviews)." )
88+ } else {
89+ fmt .Fprintln (os .Stderr , "ℹ️ gh not authenticated — commits only (run `gh auth login` for PRs & reviews)." )
90+ }
91+ }
5992
6093 opts := collect.Options {
6194 Repos : repos ,
@@ -85,12 +118,25 @@ func Run(c Config) error {
85118 return err
86119 }
87120 if len (items ) == 0 {
88- return fmt .Errorf ("nothing found for \" %s\" in range (%s)" , author , rng .Label )
121+ var b strings.Builder
122+ fmt .Fprintf (& b , "nothing found for \" %s\" in range (%s)\n " , author , rng .Label )
123+ b .WriteString (" • try a wider range, e.g. --since \" 30 days ago\" \n " )
124+ b .WriteString (" • check the name matches your commits: --author \" Your Name\" \n " )
125+ switch {
126+ case c .NoPR :
127+ b .WriteString (" • drop --no-pr to include PRs & reviews" )
128+ case ghUser == "" :
129+ b .WriteString (" • authenticate gh (`gh auth login`) to include PRs & reviews" )
130+ default :
131+ b .WriteString (" • PR/review activity may live on a different remote — those are scanned automatically" )
132+ }
133+ return fmt .Errorf ("%s" , b .String ())
89134 }
90135
91- // Pick
136+ // Pick. --all and --copy both take everything; --copy is meant to be a
137+ // one-shot "scan and copy the lot", so it skips the picker entirely.
92138 selected := items
93- if ! c .All {
139+ if ! c .All && ! c . Copy {
94140 if ! interactive {
95141 return fmt .Errorf ("no TTY for the picker; re-run with --all or in a terminal" )
96142 }
@@ -112,7 +158,7 @@ func Run(c Config) error {
112158 content = render .JSON (selected , meta )
113159 } else {
114160 content = render .Markdown (selected , meta )
115- if ! c .NoEdit && interactive {
161+ if ! c .NoEdit && interactive && ! c . Copy {
116162 edited , canceled , err := tui .Edit (content )
117163 if err != nil {
118164 return err
0 commit comments