@@ -2,18 +2,33 @@ package main
22
33import (
44 "context"
5+ "encoding/json"
56 "fmt"
67 "os"
78 "path/filepath"
89 "strings"
10+ "sort"
911
1012 "github.com/urfave/cli/v3"
1113)
1214
15+ type DirectoryInfo struct {
16+ Suffix string `json:"suffix"`
17+ Path string `json:"path"`
18+ FullPath string `json:"full_path"`
19+ IsCommand bool `json:"is_command"`
20+ IsInternal bool `json:"is_internal"`
21+ }
22+
23+ type DetectionResult struct {
24+ RootDir string `json:"root_dir"`
25+ Directories []* DirectoryInfo `json:"directories"`
26+ }
27+
1328func main () {
1429 app := & cli.Command {
1530 Name : "goScrap" ,
16- Usage : "Detects Go CLI programs and generates appropriate go build commands" ,
31+ Usage : "Detects Go CLI programs and generates appropriate go build or install commands" ,
1732 Flags : []cli.Flag {
1833 & cli.BoolFlag {
1934 Name : "verbose" ,
@@ -31,6 +46,12 @@ func main() {
3146 Usage : "Specify output directory or file for go build commands" ,
3247 Value : "" ,
3348 },
49+ & cli.BoolFlag {
50+ Name : "json" ,
51+ Aliases : []string {"j" },
52+ Usage : "Output results in JSON format" ,
53+ Value : false ,
54+ },
3455 & cli.BoolFlag {
3556 Name : "relative" ,
3657 Usage : "Use relative paths in build commands (default: absolute paths)" ,
@@ -39,6 +60,25 @@ func main() {
3960 },
4061 Action : detectAction ,
4162 },
63+ {
64+ Name : "install" ,
65+ Usage : "Generate go install commands for detected CLI programs" ,
66+ Flags : []cli.Flag {
67+ & cli.StringFlag {
68+ Name : "target" ,
69+ Aliases : []string {"t" },
70+ Usage : "Specify target version or branch (latest, main, master)" ,
71+ Value : "latest" ,
72+ },
73+ & cli.BoolFlag {
74+ Name : "json" ,
75+ Aliases : []string {"j" },
76+ Usage : "Output results in JSON format" ,
77+ Value : false ,
78+ },
79+ },
80+ Action : installAction ,
81+ },
4282 },
4383 }
4484
@@ -52,6 +92,7 @@ func detectAction(ctx context.Context, c *cli.Command) error {
5292 verbose := c .Bool ("verbose" )
5393 output := c .String ("output" )
5494 useRelative := c .Bool ("relative" )
95+ useJSON := c .Bool ("json" )
5596 rootDir := c .Args ().First ()
5697 if rootDir == "" {
5798 var err error
@@ -79,9 +120,92 @@ func detectAction(ctx context.Context, c *cli.Command) error {
79120 return fmt .Errorf ("%s is not a directory" , absRootDir )
80121 }
81122
82- var buildCommands []string
123+ result , err := detectGoCLIs (absRootDir , currentDir , output , useRelative , verbose )
124+ if err != nil {
125+ return err
126+ }
127+
128+ if len (result .Directories ) == 0 {
129+ return fmt .Errorf ("no valid Go CLI programs found in %s" , absRootDir )
130+ }
131+
132+ if useJSON {
133+ outputJSON , err := json .MarshalIndent (result , "" , " " )
134+ if err != nil {
135+ return fmt .Errorf ("failed to marshal JSON: %w" , err )
136+ }
137+ fmt .Println (string (outputJSON ))
138+ return nil
139+ }
140+
141+ for _ , dir := range result .Directories {
142+ outputPath := generateOutputPath (dir .FullPath , output , filepath .Base (dir .FullPath ), absRootDir , useRelative )
143+ fmt .Println (generateBuildCommand (dir .FullPath , outputPath , useRelative ))
144+ }
145+ return nil
146+ }
147+
148+ func installAction (ctx context.Context , c * cli.Command ) error {
149+ verbose := c .Bool ("verbose" )
150+ target := c .String ("target" )
151+ useJSON := c .Bool ("json" )
152+ rootDir := c .Args ().First ()
153+ if rootDir == "" {
154+ var err error
155+ rootDir , err = os .Getwd ()
156+ if err != nil {
157+ return fmt .Errorf ("failed to get current directory: %w" , err )
158+ }
159+ }
160+
161+ absRootDir , err := filepath .Abs (rootDir )
162+ if err != nil {
163+ return fmt .Errorf ("failed to get absolute path: %w" , err )
164+ }
165+
166+ info , err := os .Stat (absRootDir )
167+ if err != nil {
168+ return fmt .Errorf ("invalid root directory: %w" , err )
169+ }
170+ if ! info .IsDir () {
171+ return fmt .Errorf ("%s is not a directory" , absRootDir )
172+ }
173+
174+ result , err := detectGoCLIs (absRootDir , absRootDir , "" , false , verbose )
175+ if err != nil {
176+ return err
177+ }
178+
179+ if len (result .Directories ) == 0 {
180+ return fmt .Errorf ("no valid Go CLI programs found in %s" , absRootDir )
181+ }
182+
183+ goModPath , err := findGoModPath (absRootDir )
184+ if err != nil {
185+ return fmt .Errorf ("failed to find go.mod: %w" , err )
186+ }
187+
188+ if useJSON {
189+ outputJSON , err := json .MarshalIndent (result , "" , " " )
190+ if err != nil {
191+ return fmt .Errorf ("failed to marshal JSON: %w" , err )
192+ }
193+ fmt .Println (string (outputJSON ))
194+ return nil
195+ }
196+
197+ for _ , dir := range result .Directories {
198+ fmt .Println (generateInstallCommand (goModPath , dir .FullPath , target ))
199+ }
200+ return nil
201+ }
202+
203+ func detectGoCLIs (rootDir , currentDir , output string , useRelative , verbose bool ) (* DetectionResult , error ) {
204+ var result DetectionResult
205+ result .RootDir = rootDir
83206 hasGoFiles := false
84- err = filepath .Walk (absRootDir , func (path string , info os.FileInfo , err error ) error {
207+
208+ err := filepath .Walk (rootDir , func (path string , info os.FileInfo , err error ) error {
85209 if err != nil {
86210 return err
87211 }
@@ -92,39 +216,47 @@ func detectAction(ctx context.Context, c *cli.Command) error {
92216 }
93217 if isValidGoCLIDir (path , verbose ) {
94218 hasGoFiles = true
95- // use relative path or not?
96- var cmdPath string
97- if useRelative {
98- cmdPath , err = filepath .Rel (currentDir , path )
99- if err != nil {
100- return fmt .Errorf ("failed to get relative path from %s to %s: %w" , currentDir , path , err )
101- }
102- if cmdPath == "." {
103- cmdPath = ""
104- }
105- } else {
106- cmdPath = path
219+ dirInfo := & DirectoryInfo {
220+ FullPath : path ,
221+ IsCommand : true ,
222+ }
223+ if strings .Contains (path , "/internal/" ) || strings .HasPrefix (filepath .Base (path ), "internal" ) {
224+ dirInfo .IsInternal = true
107225 }
108- outputPath := generateOutputPath (path , output , info .Name (), absRootDir , useRelative )
109- cmd := generateBuildCommand (cmdPath , outputPath , useRelative )
110- buildCommands = append (buildCommands , cmd )
226+ relPath , err := filepath .Rel (rootDir , path )
227+ if err != nil {
228+ return fmt .Errorf ("failed to get relative path from %s to %s: %w" , rootDir , path , err )
229+ }
230+ dirInfo .Path = relPath
231+ dirInfo .Suffix = strings .TrimPrefix (relPath , string (os .PathSeparator ))
232+ if dirInfo .Path == "." {
233+ dirInfo .Path = filepath .Base (path )
234+ dirInfo .Suffix = filepath .Base (path )
235+ }
236+ result .Directories = append (result .Directories , dirInfo )
111237 }
112238 }
113239 return nil
114240 })
115241
116242 if err != nil {
117- return fmt .Errorf ("error walking directory: %w" , err )
243+ return nil , fmt .Errorf ("error walking directory: %w" , err )
118244 }
119245
120246 if ! hasGoFiles {
121- return fmt . Errorf ( "no valid Go CLI programs found in %s" , absRootDir )
247+ return & result , nil
122248 }
123249
124- for _ , cmd := range buildCommands {
125- fmt .Println (cmd )
250+ for i , dir := range result .Directories {
251+ if dir .Suffix == "." {
252+ result .Directories [i ].Suffix = filepath .Base (dir .Path )
253+ }
126254 }
127- return nil
255+ sort .Slice (result .Directories , func (i , j int ) bool {
256+ return result .Directories [i ].Suffix < result .Directories [j ].Suffix
257+ })
258+
259+ return & result , nil
128260}
129261
130262func isExcludedDir (name string ) bool {
@@ -146,7 +278,6 @@ func isValidGoCLIDir(dir string, verbose bool) bool {
146278 if err != nil {
147279 return err
148280 }
149- // dont check subdirs
150281 if info .IsDir () && path != dir {
151282 return filepath .SkipDir
152283 }
@@ -165,7 +296,6 @@ func isValidGoCLIDir(dir string, verbose bool) bool {
165296 if strings .HasPrefix (trimmed , "package main" ) {
166297 hasMain = true
167298 }
168- // overkill? Maybe...
169299 if strings .Contains (trimmed , "func main()" ) {
170300 hasFuncMain = true
171301 }
@@ -183,6 +313,38 @@ func isValidGoCLIDir(dir string, verbose bool) bool {
183313 return hasMain && hasFuncMain && hasValidGoFiles
184314}
185315
316+ func findGoModPath (rootDir string ) (string , error ) {
317+ var goModPath string
318+ err := filepath .Walk (rootDir , func (path string , info os.FileInfo , err error ) error {
319+ if err != nil {
320+ return err
321+ }
322+ if ! info .IsDir () && info .Name () == "go.mod" {
323+ goModPath = path
324+ return filepath .SkipDir
325+ }
326+ return nil
327+ })
328+ if err != nil {
329+ return "" , fmt .Errorf ("error searching for go.mod: %w" , err )
330+ }
331+ if goModPath == "" {
332+ return "" , fmt .Errorf ("no go.mod file found in %s or its subdirectories" , rootDir )
333+ }
334+
335+ content , err := os .ReadFile (goModPath )
336+ if err != nil {
337+ return "" , fmt .Errorf ("failed to read go.mod: %w" , err )
338+ }
339+ lines := strings .Split (string (content ), "\n " )
340+ for _ , line := range lines {
341+ if strings .HasPrefix (strings .TrimSpace (line ), "module " ) {
342+ return strings .TrimSpace (strings .TrimPrefix (line , "module " )), nil
343+ }
344+ }
345+ return "" , fmt .Errorf ("no module path found in go.mod" )
346+ }
347+
186348func generateOutputPath (dir , output , dirName , rootDir string , useRelative bool ) string {
187349 if output == "" {
188350 return filepath .Join (dir , dirName )
@@ -214,3 +376,13 @@ func generateBuildCommand(cmdPath, outputPath string, useRelative bool) string {
214376 }
215377 return fmt .Sprintf ("go build -C %s -o %s" , cmdPath , outputPath )
216378}
379+
380+ func generateInstallCommand (modulePath , cmdPath , target string ) string {
381+ suffix := strings .TrimPrefix (cmdPath , filepath .Dir (modulePath ))
382+ if suffix == "" || suffix == "." {
383+ suffix = "/cmd/" + filepath .Base (cmdPath )
384+ } else {
385+ suffix = "/cmd/" + strings .TrimPrefix (suffix , string (os .PathSeparator ))
386+ }
387+ return fmt .Sprintf ("go install %s%s@%s" , modulePath , suffix , target )
388+ }
0 commit comments