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
2 changes: 1 addition & 1 deletion cmd/goini/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func runNew(cmd *cobra.Command, opts *newOpts) error {

fmt.Fprintf(cmd.OutOrStdout(), "Generating %s project…\n", req.ProjectType)

zipBuf, err := gen.Generate(req)
zipBuf, err := gen.Generate(cmd.Context(), req)
if err != nil {
return fmt.Errorf("generation failed: %w", err)
}
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module github.com/neo7337/go-initializer

go 1.24.4
go 1.25.0

require (
github.com/dave/jennifer v1.7.1
github.com/stretchr/testify v1.10.0
golang.org/x/time v0.15.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
24 changes: 20 additions & 4 deletions internal/generator/gen_ai_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package generator
import (
"archive/zip"
"bytes"
"context"
"fmt"
"log"
)
Expand All @@ -29,14 +30,21 @@ import (
// └── Dockerfile # if dockerSupport is true
type AIAgentGenerator struct{}

func (g *AIAgentGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer, error) {
func (g *AIAgentGenerator) Generate(ctx context.Context, request CreateProjectRequest) (*bytes.Buffer, error) {
if request.Name == "" {
request.Name = "myagent"
}
if err := ValidateProjectName(request.Name); err != nil {
return nil, err
}
if err := ValidateModuleName(request.ModuleName); err != nil {
return nil, err
}

buf := new(bytes.Buffer)
zipWriter := zip.NewWriter(buf)

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

// README.md
readmeContent := fmt.Sprintf("# %s\n\n%s", folderName, request.Description)
Expand All @@ -56,6 +64,10 @@ func (g *AIAgentGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer
return nil, err
}

if err := ctx.Err(); err != nil {
return nil, err
}

// main.go — thin entrypoint that calls agent.Run()
if err := addToZip(zipWriter, fmt.Sprintf("%s/main.go", folderName), generateAIAgentMain(request.ModuleName)); err != nil {
log.Printf("[ERROR] %v", err)
Expand Down Expand Up @@ -121,6 +133,10 @@ func (g *AIAgentGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer
}

// Makefile — main package is at the project root "."
if err := ctx.Err(); err != nil {
return nil, err
}

if err := addToZip(zipWriter, fmt.Sprintf("%s/Makefile", folderName), GenerateMakefile(folderName, ".")); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
Expand Down
24 changes: 20 additions & 4 deletions internal/generator/gen_cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package generator
import (
"archive/zip"
"bytes"
"context"
"fmt"
"log"
)
Expand All @@ -24,14 +25,21 @@ import (
// └── README.md
type CLIAppGenerator struct{}

func (g *CLIAppGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer, error) {
func (g *CLIAppGenerator) Generate(ctx context.Context, request CreateProjectRequest) (*bytes.Buffer, error) {
if request.Name == "" {
request.Name = "mycli"
}
if err := ValidateProjectName(request.Name); err != nil {
return nil, err
}
if err := ValidateModuleName(request.ModuleName); err != nil {
return nil, err
}

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)
Expand All @@ -51,6 +59,10 @@ func (g *CLIAppGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer,
return nil, err
}

if err := ctx.Err(); err != nil {
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)
Expand Down Expand Up @@ -97,6 +109,10 @@ func (g *CLIAppGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer,
}

// Makefile — main package is at the project root "."
if err := ctx.Err(); err != nil {
return nil, err
}

if err := addToZip(zipWriter, fmt.Sprintf("%s/Makefile", folderName), GenerateMakefile(folderName, ".")); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
Expand Down
24 changes: 20 additions & 4 deletions internal/generator/gen_microservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package generator
import (
"archive/zip"
"bytes"
"context"
"fmt"
"log"
)
Expand All @@ -27,14 +28,21 @@ import (
// └── Dockerfile # if dockerSupport is true
type MicroserviceGenerator struct{}

func (g *MicroserviceGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer, error) {
func (g *MicroserviceGenerator) Generate(ctx context.Context, request CreateProjectRequest) (*bytes.Buffer, error) {
if request.Name == "" {
request.Name = "myservice"
}
if err := ValidateProjectName(request.Name); err != nil {
return nil, err
}
if err := ValidateModuleName(request.ModuleName); err != nil {
return nil, err
}

buf := new(bytes.Buffer)
zipWriter := zip.NewWriter(buf)

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

// README.md
readmeContent := fmt.Sprintf("# %s\n\n%s", folderName, request.Description)
Expand All @@ -54,6 +62,10 @@ func (g *MicroserviceGenerator) Generate(request CreateProjectRequest) (*bytes.B
return nil, err
}

if err := ctx.Err(); err != nil {
return nil, err
}

// cmd/<name>/main.go — framework-aware
mainContent, err := GenerateMainContent(request.Framework)
if err != nil {
Expand Down Expand Up @@ -111,6 +123,10 @@ func (g *MicroserviceGenerator) Generate(request CreateProjectRequest) (*bytes.B
}

// Makefile
if err := ctx.Err(); err != nil {
return nil, err
}

if err := addToZip(zipWriter, fmt.Sprintf("%s/Makefile", folderName), GenerateMakefile(folderName, fmt.Sprintf("./cmd/%s", folderName))); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
Expand Down
24 changes: 20 additions & 4 deletions internal/generator/gen_simple_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package generator
import (
"archive/zip"
"bytes"
"context"
"fmt"
"log"
)
Expand All @@ -19,14 +20,21 @@ import (
// └── Dockerfile (optional)
type SimpleProjectGenerator struct{}

func (g *SimpleProjectGenerator) Generate(request CreateProjectRequest) (*bytes.Buffer, error) {
func (g *SimpleProjectGenerator) Generate(ctx context.Context, request CreateProjectRequest) (*bytes.Buffer, error) {
if request.Name == "" {
request.Name = "myproject"
}
if err := ValidateProjectName(request.Name); err != nil {
return nil, err
}
if err := ValidateModuleName(request.ModuleName); err != nil {
return nil, err
}

buf := new(bytes.Buffer)
zipWriter := zip.NewWriter(buf)

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

// README.md
readmeContent := fmt.Sprintf("# %s\n\n%s", folderName, request.Description)
Expand All @@ -46,6 +54,10 @@ func (g *SimpleProjectGenerator) Generate(request CreateProjectRequest) (*bytes.
return nil, err
}

if err := ctx.Err(); err != nil {
return nil, err
}

// cmd/<name>/main.go
mainGoContent, err := GenerateMainContent(request.Framework)
if err != nil {
Expand Down Expand Up @@ -86,6 +98,10 @@ func (g *SimpleProjectGenerator) Generate(request CreateProjectRequest) (*bytes.
}

// internal/service.go
if err := ctx.Err(); err != nil {
return nil, err
}

if err := addToZip(zipWriter, fmt.Sprintf("%s/internal/service.go", folderName), GenerateServiceContent(folderName)); err != nil {
log.Printf("[ERROR] %v", err)
return nil, err
Expand Down
47 changes: 47 additions & 0 deletions internal/generator/gen_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,58 @@ import (
"archive/zip"
"bytes"
"fmt"
"regexp"
"strings"

jen "github.com/dave/jennifer/jen"
)

var (
reProjectName = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]{0,63}$`)
reModuleName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,255}$`)
)

// ErrValidation is returned when an input field fails sanitization checks.
type ErrValidation struct {
Field string
Message string
}

func (e *ErrValidation) Error() string {
return fmt.Sprintf("invalid %s: %s", e.Field, e.Message)
}

// ValidateProjectName checks that name is safe for use as a filesystem folder name.
// Rejects path-traversal sequences (/, \, .., null bytes) and enforces the pattern
// ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$.
func ValidateProjectName(name string) error {
if strings.ContainsAny(name, "/\\") || strings.Contains(name, "..") || strings.ContainsRune(name, 0) {
return &ErrValidation{Field: "name", Message: "contains disallowed characters (/, \\, .., or null bytes)"}
}
if !reProjectName.MatchString(name) {
return &ErrValidation{Field: "name", Message: `must match ^[a-zA-Z][a-zA-Z0-9_-]{0,63}$`}
}
return nil
}

// ValidateModuleName checks that a Go module path is safe.
// Rejects backslashes, null bytes, and dotdot path segments, and enforces the pattern
// ^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,255}$.
func ValidateModuleName(moduleName string) error {
if strings.Contains(moduleName, "\\") || strings.ContainsRune(moduleName, 0) {
return &ErrValidation{Field: "moduleName", Message: "contains disallowed characters (backslash or null bytes)"}
}
for _, seg := range strings.Split(moduleName, "/") {
if seg == ".." {
return &ErrValidation{Field: "moduleName", Message: "contains path traversal sequence (..)"}
}
}
if !reModuleName.MatchString(moduleName) {
return &ErrValidation{Field: "moduleName", Message: `must match ^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,255}$`}
}
return nil
}

// addToZip writes content into a new zip entry at path, eliminating the
// repeated create-then-write boilerplate across all generators.
func addToZip(zw *zip.Writer, path string, content []byte) error {
Expand Down
Loading
Loading