Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions backend/gen_cli_app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package main

import (
"archive/zip"
"bytes"
"fmt"
"log"
)

// CLIAppGenerator implements Generator for the "cli-app" project type.
//
// Generated layout:
//
// <name>/
// ├── main.go # thin entry point — calls cmd.Execute()
// ├── cmd/
// │ ├── root.go # root command + Execute() function
// │ └── <name>.go # sample sub-command
// ├── internal/
// │ └── logger/logger.go # if zap/logrus addon selected
// ├── go.mod
// ├── .gitignore
// ├── Makefile
// └── README.md
type CLIAppGenerator struct{}

func (g *CLIAppGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer, error) {
buf := new(bytes.Buffer)
zipWriter := zip.NewWriter(buf)

folderName := request.Name
if folderName == "" {
folderName = "mycli"
}

// README.md
readmeContent := fmt.Sprintf("# %s\n\n%s", folderName, request.Description)
if err := addToZip(zipWriter, fmt.Sprintf("%s/README.md", folderName), []byte(readmeContent)); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

// go.mod
gomodContent, err := GenerateGoModV2(request)
if err != nil {
log.Printf("[ERROR] Failed to generate go.mod: %v", err)
return nil, fmt.Errorf("failed to generate go.mod: %w", err)
}
if err := addToZip(zipWriter, fmt.Sprintf("%s/go.mod", folderName), gomodContent); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

// main.go — always delegates to cmd.Execute()
if err := addToZip(zipWriter, fmt.Sprintf("%s/main.go", folderName), generateCLIMain(request.ModuleName)); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

// cmd/root.go — root command definition + Execute() entry point
if err := addToZip(zipWriter, fmt.Sprintf("%s/cmd/root.go", folderName), GenerateRootCmd(request.Framework, folderName)); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

// cmd/<name>.go — sample sub-command
if err := addToZip(zipWriter, fmt.Sprintf("%s/cmd/%s.go", folderName, folderName), GenerateSubCmd(request.Framework, folderName)); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

// Addons — "other" (logging) only; cache/database pass through the addon registry
for addonType, addons := range request.Addons {
if len(addons) == 0 {
continue
}
if addonType == "other" {
loggerContent, err := GenerateLoggingAddon(addons)
if err != nil {
log.Printf("[WARN] Skipping logging addon: %v", err)
continue
}
if err := addToZip(zipWriter, fmt.Sprintf("%s/internal/logger/logger.go", folderName), loggerContent); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}
continue
}
gen, ok := addonRegistry[addonType]
if !ok {
continue
}
if err := gen.Generate(folderName, addons, zipWriter); err != nil {
log.Printf("[ERROR] Failed to generate %s addon: %v", addonType, err)
return nil, fmt.Errorf("failed to generate %s addon: %w", addonType, err)
}
}

// Makefile — main package is at the project root "."
if err := addToZip(zipWriter, fmt.Sprintf("%s/Makefile", folderName), GenerateMakefile(folderName, ".")); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

// .gitignore
if err := addToZip(zipWriter, fmt.Sprintf("%s/.gitignore", folderName), GenerateGitignore()); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
}

if err := zipWriter.Close(); err != nil {
log.Printf("[ERROR] Failed to close zip writer: %v", err)
return nil, fmt.Errorf("failed to finalize zip: %w", err)
}
return buf, nil
}

// generateCLIMain returns a thin main.go that delegates to cmd.Execute().
// All frameworks use the same pattern since the framework-specific logic lives
// entirely within the cmd package.
func generateCLIMain(moduleName string) []byte {
return []byte(fmt.Sprintf(`// Code generated by go-initializer. DO NOT EDIT.
package main

import "%s/cmd"

func main() {
cmd.Execute()
}
`, moduleName))
}
182 changes: 182 additions & 0 deletions backend/gen_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,188 @@ func (s *` + lower + `Service) Hello() (string, error) {
`)
}

// GenerateRootCmd returns a framework-specific cmd/root.go that defines the root
// command and an Execute() entry point. name is used as the binary/command name.
func GenerateRootCmd(framework, name string) []byte {
title := strings.ToUpper(name[:1]) + name[1:]
switch framework {
case "cobra":
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "` + name + `",
Short: "A CLI application",
Long: "` + title + ` is a CLI application built with cobra.",
}

// Execute runs the root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func init() {
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "enable verbose output")
}
`)
case "urfave":
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import (
"fmt"
"os"

"github.com/urfave/cli/v2"
)

// Execute builds the CLI application and runs it.
func Execute() {
app := &cli.App{
Name: "` + name + `",
Usage: "A CLI application",
Commands: []*cli.Command{
` + title + `Command(),
},
Action: func(c *cli.Context) error {
fmt.Println("Run '` + name + ` help' for usage.")
return nil
},
}
if err := app.Run(os.Args); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
`)
case "kingpin":
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import (
"os"

"gopkg.in/alecthomas/kingpin.v2"
)

// App is the kingpin application instance shared across all command files.
var App = kingpin.New("` + name + `", "A CLI application.")

// Execute parses arguments and dispatches the selected command.
func Execute() {
kingpin.MustParse(App.Parse(os.Args[1:]))
}
`)
default: // golly — no dedicated CLI framework; use golly logger
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import "oss.nandlabs.io/golly/l3"

var logger = l3.Get()

// Execute is the entry point for the CLI application.
func Execute() {
logger.Info("` + name + ` started")
run` + title + `()
}
`)
}
}

// GenerateSubCmd returns a framework-specific cmd/<name>.go that defines a sample
// sub-command wired into the root command produced by GenerateRootCmd.
func GenerateSubCmd(framework, name string) []byte {
title := strings.ToUpper(name[:1]) + name[1:]
switch framework {
case "cobra":
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var ` + name + `Cmd = &cobra.Command{
Use: "` + name + `",
Short: "A brief description of the ` + name + ` command",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("` + name + ` called")
},
}

func init() {
rootCmd.AddCommand(` + name + `Cmd)
}
`)
case "urfave":
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import (
"fmt"

"github.com/urfave/cli/v2"
)

// ` + title + `Command returns the ` + name + ` sub-command definition.
func ` + title + `Command() *cli.Command {
return &cli.Command{
Name: "` + name + `",
Usage: "A brief description of the ` + name + ` command",
Action: func(c *cli.Context) error {
fmt.Println("` + name + ` called")
return nil
},
}
}
`)
case "kingpin":
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import (
"fmt"

"gopkg.in/alecthomas/kingpin.v2"
)

func init() {
` + name + `Cmd := App.Command("` + name + `", "A brief description of the ` + name + ` command")
` + name + `Cmd.Action(func(_ *kingpin.ParseContext) error {
fmt.Println("` + name + ` called")
return nil
})
}
`)
default: // golly
return []byte(`// Code generated by go-initializer. DO NOT EDIT.
package cmd

import "oss.nandlabs.io/golly/l3"

var subLogger = l3.Get()

// run` + title + ` performs the ` + name + ` operation.
func run` + title + `() {
subLogger.Info("running ` + name + `")
}
`)
}
}

// GenerateLoggingAddon returns an internal/logger/logger.go file initialised for
// zap or logrus. The first recognised logging library found in addons is used;
// if neither is present an error is returned.
Expand Down
Binary file modified backend/go-initializer
Binary file not shown.
3 changes: 3 additions & 0 deletions backend/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type AddonGenerator interface {
var generatorRegistry = map[string]Generator{
"simple-project": &SimpleProjectGenerator{},
"microservice": &MicroserviceGenerator{},
"cli-app": &CLIAppGenerator{},
}

// addonRegistry maps addon category names to their AddonGenerator implementations.
Expand Down Expand Up @@ -105,6 +106,8 @@ var DependencyMap = map[string][]string{
"zerolog": {"github.com/rs/zerolog v1.34.0"},
"viper": {"github.com/spf13/viper v1.21.0"},
"cobra": {"github.com/spf13/cobra v1.10.2", "github.com/spf13/pflag v1.0.10"},
"urfave": {"github.com/urfave/cli/v2 v2.27.6"},
"kingpin": {"gopkg.in/alecthomas/kingpin.v2 v2.2.6"},
"testify": {"github.com/stretchr/testify v1.11.1"},
"httptest": {"net/http/httptest"},
"redis": {"github.com/redis/go-redis/v9 v9.17.2"},
Expand Down
Loading
Loading