Skip to content
Open
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
14 changes: 10 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,17 @@ First off, thank you for considering contributing to `goscaf`! It's people like
4. Build the project: `make build`.

## Project Structure
- `main.go`: Entry point.
- `main.go`: Entry point — delegates immediately to `cmd.Execute()`.
- `cmd/`: CLI commands (Cobra-based).
- `internal/`: Core logic and templates.
- `internal/generator/`: Logic for generating files.
- `internal/templates/`: Project boilerplates.
- `cmd/root.go`: Root command and banner.
- `cmd/init.go`: `goscaf init` — collects config and drives the generator.
- `cmd/add.go`: `goscaf add` — adds a new service scaffold to an existing project.
- `internal/`: Core logic — not importable by external packages.
- `internal/config/`: `ProjectConfig` struct and typed constants for framework, logger, and database choices.
- `internal/userconfig/`: Loads and merges `~/.goscaf.yaml` (global) and `./.goscaf.yaml` (local) defaults.
- `internal/prompt/`: Interactive survey prompts; accepts a `UserConfig` to pre-fill defaults.
- `internal/generator/`: Orchestrates file writes for `goscaf init` and `goscaf add`.
- `internal/templates/`: Go `text/template` strings for every generated file.

## Style Guide
- We follow standard Go idioms.
Expand Down
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,105 @@ goscaf init my-api --framework fiber --logger zap --redis --kafka --docker

---

## Config file (.goscaf.yaml)

Tired of answering the same prompts every time? Save your preferences once in a `.goscaf.yaml` file and `goscaf` will pre-fill every prompt with your defaults. You can still change any value at the prompt — the config just saves you the typing.

### Two scopes, same format

| File | Scope | Who should set it |
|---|---|---|
| `~/.goscaf.yaml` | Global — applies to all projects on this machine | Individual developers |
| `./.goscaf.yaml` | Local — applies only to the current directory | Teams (commit to the repo) |

When both files exist, **local values win over global**. CLI flags always win over both. The full priority chain is:

```
hardcoded defaults < ~/.goscaf.yaml < ./.goscaf.yaml < CLI flags
```

### All supported fields

```yaml
# .goscaf.yaml

# Module prefix — goscaf appends /<project-name> automatically.
# Example: "github.com/your-org" produces "github.com/your-org/my-api"
module_prefix: github.com/your-org

# Go toolchain version to write into go.mod
go_version: "1.25.0"

# HTTP framework: gin | fiber | chi | echo | gorilla | none
framework: gin

# Structured logger: slog | zerolog | zap
logger: slog

# Database driver: none | postgres | mysql | sqlite | mongo | gorm
db: none

# Optional infrastructure clients
viper: true
redis: false
kafka: false
nats: false

# DevOps scaffolding
docker: true
makefile: true
github: true
lint: true
swagger: false
git_repo: false
```

Any field you omit falls back to the next level in the priority chain — you only need to set what you actually want to override.

### Common setups

**Personal global config** — sets your usual module prefix and preferred logger, nothing else:

```yaml
# ~/.goscaf.yaml
module_prefix: github.com/john-doe
logger: zap
go_version: "1.25.0"
```

**Team repo config** — standardises the stack across all services; commit this file so every engineer gets the same defaults:

```yaml
# .goscaf.yaml (committed to the monorepo root)
module_prefix: github.com/acme-corp
framework: chi
logger: zerolog
viper: true
docker: true
makefile: true
github: true
lint: true
swagger: false
```

**Override a single field at the CLI** — team config says `framework: chi`, but you need fiber just this once:

```bash
goscaf init payments-service --framework fiber
```

The rest of the fields still come from `.goscaf.yaml`; only `framework` is overridden.

### What happens in each mode

| Mode | Config file applied? |
|---|---|
| Interactive (`goscaf init <name>`) | Yes — pre-fills prompt defaults |
| Flag-driven (`--framework`, `--logger`, …) | Yes — fills any field not covered by a flag |
| Defaults (`--defaults`) | No — hardcoded defaults only, config ignored |

---

## Flags

| Flag | Default | Description |
Expand Down
Binary file modified bin/goscaf
Binary file not shown.
77 changes: 70 additions & 7 deletions cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/iyashjayesh/goscaf/internal/config"
"github.com/iyashjayesh/goscaf/internal/generator"
"github.com/iyashjayesh/goscaf/internal/prompt"
"github.com/iyashjayesh/goscaf/internal/userconfig"
)

var (
Expand Down Expand Up @@ -45,6 +46,13 @@ var initCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
projectName := args[0]

// Load .goscaf.yaml (global then local, local wins). A missing file is
// not an error — uc will be nil and prompts fall back to hardcoded defaults.
uc, err := userconfig.Load()
if err != nil {
color.Yellow(" ⚠ could not read .goscaf.yaml: %v", err)
}

var cfg *config.ProjectConfig

if flagDefaults {
Expand All @@ -70,7 +78,8 @@ var initCmd = &cobra.Command{
} else if cmd.Flags().Changed("framework") || cmd.Flags().Changed("module") ||
cmd.Flags().Changed("go-version") || cmd.Flags().Changed("logger") ||
cmd.Flags().Changed("db") {
// Flags provided - use flag-driven mode (merge with defaults)
// Flags provided — start from flag values, then fill in any field the
// user did NOT explicitly set from .goscaf.yaml (flags always win).
cfg = &config.ProjectConfig{
ProjectName: projectName,
ModuleName: flagModule,
Expand All @@ -90,14 +99,21 @@ var initCmd = &cobra.Command{
GitRepo: flagGitRepo,
}
if cfg.ModuleName == "" {
cfg.ModuleName = fmt.Sprintf("github.com/your-org/%s", projectName)
if uc != nil && uc.ModulePrefix != "" {
cfg.ModuleName = uc.ModulePrefix + "/" + projectName
} else {
cfg.ModuleName = fmt.Sprintf("github.com/your-org/%s", projectName)
}
}
if uc != nil {
applyUserConfig(cmd, cfg, uc)
}
} else {
// Interactive mode
var err error
cfg, err = prompt.Run(projectName)
if err != nil {
return fmt.Errorf("prompt failed: %w", err)
// Interactive mode — userconfig pre-fills prompt defaults.
var promptErr error
cfg, promptErr = prompt.Run(projectName, uc)
if promptErr != nil {
return fmt.Errorf("prompt failed: %w", promptErr)
}
}

Expand Down Expand Up @@ -148,6 +164,53 @@ var initCmd = &cobra.Command{
},
}

// applyUserConfig fills cfg fields from uc for any flag the user did not
// explicitly pass on the command line. CLI flags always take precedence.
func applyUserConfig(cmd *cobra.Command, cfg *config.ProjectConfig, uc *userconfig.UserConfig) {
if !cmd.Flags().Changed("go-version") && uc.GoVersion != "" {
cfg.GoVersion = uc.GoVersion
}
if !cmd.Flags().Changed("framework") && uc.Framework != "" {
cfg.Framework = config.Framework(uc.Framework)
}
if !cmd.Flags().Changed("logger") && uc.Logger != "" {
cfg.Logger = config.Logger(uc.Logger)
}
if !cmd.Flags().Changed("db") && uc.DB != "" {
cfg.Database = config.Database(uc.DB)
}
if !cmd.Flags().Changed("viper") && uc.Viper != nil {
cfg.Viper = *uc.Viper
}
if !cmd.Flags().Changed("redis") && uc.Redis != nil {
cfg.Redis = *uc.Redis
}
if !cmd.Flags().Changed("kafka") && uc.Kafka != nil {
cfg.Kafka = *uc.Kafka
}
if !cmd.Flags().Changed("nats") && uc.NATS != nil {
cfg.NATS = *uc.NATS
}
if !cmd.Flags().Changed("docker") && uc.Docker != nil {
cfg.Docker = *uc.Docker
}
if !cmd.Flags().Changed("makefile") && uc.Makefile != nil {
cfg.Makefile = *uc.Makefile
}
if !cmd.Flags().Changed("github") && uc.GitHub != nil {
cfg.GitHub = *uc.GitHub
}
if !cmd.Flags().Changed("lint") && uc.Lint != nil {
cfg.Lint = *uc.Lint
}
if !cmd.Flags().Changed("swagger") && uc.Swagger != nil {
cfg.Swagger = *uc.Swagger
}
if !cmd.Flags().Changed("git-repo") && uc.GitRepo != nil {
cfg.GitRepo = *uc.GitRepo
}
}

func init() {
rootCmd.AddCommand(initCmd)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ require (
golang.org/x/sys v0.42.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading