fuzzyworld

template / commit

improvements to the repo, 1-5

37160b896bed0d8cf918ad91664991340559b099 · Pavel Safronov · 2026-05-01 10:24:46 -0700 · parents 04989558f885

improvements to the repo, 1-5
 .goreleaser.yaml                                   |   3 +-
 .vscode/settings.json                              |   3 +
 IMPROVEMENTS.md                                    | 149 +++++------
 Makefile                                           |   4 +-
 go.mod                                             |   1 -
 go.sum                                             |   2 -
 main.go => internal/core/app.go                    | 274 +++++++++------------
 config.go => internal/core/config.go               |   4 +-
 config_test.go => internal/core/config_test.go     |   2 +-
 .../core/default_config.go                         |   2 +-
 .../core/defaultconfig_test.go                     |   2 +-
 extras.go => internal/core/extras.go               |   2 +-
 extras_test.go => internal/core/extras_test.go     |   2 +-
 file.go => internal/core/file.go                   |   2 +-
 file_test.go => internal/core/file_test.go         |   2 +-
 main_test.go => internal/core/main_test.go         |  10 +-
 monkey-patch.go => internal/core/monkey-patch.go   |   2 +-
 name_pattern.go => internal/core/name_pattern.go   |   2 +-
 .../core/name_pattern_test.go                      |   4 +-
 .../core/parameter_data.go                         |   2 +-
 postscript.go => internal/core/postscript.go       |   2 +-
 .../core/postscript_test.go                        |   2 +-
 profile.go => internal/core/profile.go             |   4 +-
 profile_test.go => internal/core/profile_test.go   |   2 +-
 templating.go => internal/core/templating.go       |   2 +-
 test-common.go => internal/core/test-common.go     |   2 +-
 utils.go => internal/core/utils.go                 |   2 +-
 utils_test.go => internal/core/utils_test.go       |   2 +-
 28 files changed, 213 insertions(+), 279 deletions(-)

diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index a9bd3d7..220c59d 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -6,7 +6,8 @@ before:
     # You may remove this if you don't use go modules.
     - go mod tidy
 builds:
-  - env:
+  - main: ./cmd/template/
+    env:
       - CGO_ENABLED=0
     goos:
       - linux
diff --git a/.vscode/settings.json b/.vscode/settings.json
index c83a831..d170926 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,6 +3,9 @@
     "bashdb",
     "filetemplate",
     "hackathon",
+    "golangci",

+    "gomarklint",

+    "goreleaser",

     "logrus",
     "Pavel",
     "pwsh",
diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md
index 0a6d1d0..1152ee6 100644
--- a/IMPROVEMENTS.md
+++ b/IMPROVEMENTS.md
@@ -4,150 +4,127 @@ Ordered roughly by effort, lowest first. Each section lists the concrete changes
 
 ---
 
-## 1. Drop `golang.org/x/exp` dependency
+## ✅ 1. Drop `golang.org/x/exp` dependency
 
-`golang.org/x/exp/slices` was a backport of the stdlib `slices` package (available since
-Go 1.21). Replace every import with the stdlib package.
-
-Files to update:
-
-- [config.go](config.go) — `import "golang.org/x/exp/slices"` → `import "slices"`
-- [profile.go](profile.go) — same
-- [name_pattern_test.go](name_pattern_test.go) — same
-
-After swapping imports, run `go mod tidy` to remove the `golang.org/x/exp` entries from
-`go.mod` and `go.sum`.
+Replaced `golang.org/x/exp/slices` with the stdlib `slices` package in
+[config.go](internal/core/config.go), [profile.go](internal/core/profile.go), and
+[name_pattern_test.go](internal/core/name_pattern_test.go). Removed the dependency
+from [go.mod](go.mod) via `go mod tidy`.
 
 ---
 
-## 2. Upgrade Go version
-
-`go.mod` declares `go 1.24.0` / `toolchain go1.24.4`, but the GitLab CI image is already
-`golang:1.25`. Align `go.mod` with the CI image so the declared minimum matches what is
-actually tested against.
-
-Steps:
+## ✅ 2. Upgrade Go version
 
-1. Run `go mod tidy` after updating the `go` directive in [go.mod](go.mod).
-2. Update `toolchain` line to match the current patch release.
-3. Verify all tests still pass.
+[go.mod](go.mod) now declares `go 1.25.0`, driven by the project's actual transitive
+deps. The GitLab CI image (`golang:1.25`) is aligned.
 
 ---
 
-## 3. Reorganize source files into a nested directory
+## ✅ 3. Reorganize source files into a nested directory
 
-All `.go` files currently live at the repo root alongside config files, making the layout
-cluttered. The standard Go layout for a CLI tool is:
+All source files moved out of the repo root into:
 
 ```text
 cmd/
   template/
-    main.go          ← entry point only; wire dependencies here
+    main.go       ← 14-line entry point; sets Version, calls core.Run()
 internal/
-  core/              ← or any name: "engine", "template", etc.
+  core/
+    app.go        ← CLI wiring + all orchestration logic
     config.go
-    config_test.go
-    default_config.go
-    ...all other .go files...
+    profile.go
+    ... (all other files)
 ```
 
-Concrete steps:
+[Makefile](Makefile) build and install targets updated to `./cmd/template/`.
+[.goreleaser.yaml](.goreleaser.yaml) updated with `main: ./cmd/template/`.
 
-1. Create `cmd/template/` and `internal/core/` directories.
-2. Move `main.go` to `cmd/template/main.go`. Keep it thin — just flag parsing and calls
-   into `internal/core`.
-3. Move every other `.go` file (including test files) to `internal/core/`.
-4. Change the package declaration in all moved files from `package main` to `package core`
-   (or chosen name).
-5. Re-export only what `cmd/template/main.go` needs.
-6. Update the `Makefile` build target: `go build ./cmd/template/`.
-7. Update `.goreleaser.yaml` main path if it references a specific file.
-8. Run `go test ./...` to confirm nothing broke.
+---
+
+## ✅ 4. Tool management via separate module
 
-**Note:** `monkey-patch.go` and `test-common.go` contain test-support code. They can live
-alongside the production code in `internal/core/` (Go only compiles `_test.go` files during
-tests) or move into a `internal/core/testutil/` sub-package.
+Dev tools (golangci-lint, gomarklint, goreleaser) are now version-pinned in
+[tools/go.mod](tools/go.mod), keeping their hundreds of transitive dependencies
+out of the main [go.mod](go.mod). The [Makefile](Makefile) installs them via
+`cd tools && go install ...` and exposes `make lint` and `make lint-docs` targets.
 
 ---
 
-## 4. Improve test coverage
+## ✅ 5. Reduce `main.go` size
 
-Current tests focus on the happy path. Priority areas for new tests:
+[cmd/template/main.go](cmd/template/main.go) is now 14 lines. All CLI wiring,
+settings construction (`createSettings`), profile resolution (`findProfile`), and
+orchestration (`runMain`) live in [internal/core/app.go](internal/core/app.go)
+where they are reachable by tests without going through the CLI layer.
+
+---
 
-### 4a. Error paths in config handling ([config.go](config.go))
+## 6. Improve test coverage
+
+Current coverage: ~56%. Tests focus on the happy path. Priority areas for new tests:
+
+### 6a. Error paths in config handling ([config.go](internal/core/config.go))
 
 - `loadConfig` — file not found, malformed YAML, invalid permissions
 - `saveConfig` — write failure (read-only directory)
 - `validateConfig` — all validation branches (duplicate names, multiple defaults)
 
-### 4b. Error paths in file creation ([file.go](file.go))
+### 6b. Error paths in file creation ([file.go](internal/core/file.go))
 
-- `createFile` — template parse error, template execution error, `os.Create` failure,
-  `os.WriteFile` failure
+- `createFile` — template parse error, template execution error, `os.Create`
+  failure, `os.WriteFile` failure
 - `readFile` — file not found
 
-### 4c. Profile processing edge cases ([profile.go](profile.go))
+### 6c. Profile processing edge cases ([profile.go](internal/core/profile.go))
 
 - `processProfile` with `ScriptType` (currently untested or minimally tested)
 - `createEntity` failure cases: mkdir failure, nested file creation failure
-- `validateProfile` — all invalid combinations (missing target, unsupported OS, etc.)
+- `validateProfile` — all invalid combinations (missing target, unsupported OS,
+  etc.)
 
-### 4d. Extras loading ([extras.go](extras.go))
+### 6d. Extras loading ([extras.go](internal/core/extras.go))
 
 - Missing extras file (should not error)
 - Malformed extras YAML
 
-### 4e. `mainLogic` integration ([main.go](main.go))
+### 6e. App-level integration ([app.go](internal/core/app.go))
 
 - Profile not found
 - Config load failure
 - Piped stdin with a folder-type profile (should be an error or no-op)
 
-### 4f. Coverage tooling
+### 6f. Coverage gate
 
-Add a coverage threshold gate to the Makefile so CI fails if coverage drops below a chosen
+Add a threshold to the Makefile so CI fails if coverage drops below a chosen
 minimum (e.g., 70%):
 
 ```makefile
-test:
+test: build
     go test ./... -coverprofile=test-coverage.out
-    go tool cover -func=test-coverage.out | grep total | awk '{if ($$3+0 < 70) exit 1}'
+    go tool cover -func=test-coverage.out | grep total \
+        | awk '{if ($$3+0 < 70) exit 1}'
 ```
 
 ---
 
-## 5. Reduce `main.go` size
-
-At 584 lines, [main.go](main.go) mixes CLI wiring, settings construction, and business
-logic. After the reorganization in item 3, consider splitting it further:
-
-- Keep `main.go` (in `cmd/template/`) to flag definitions and `main()` only (~100 lines).
-- Move `createSettings()`, `mainLogic()`, and `findProfile()` into `internal/core/` where
-  they are easier to unit-test without going through the CLI layer.
-
----
-
-## 6. Clean up `default_config.go`
+## 7. Clean up `default_config.go`
 
-At 397 lines, [default_config.go](default_config.go) is unwieldy. Options:
+At 396 lines, [default_config.go](internal/core/default_config.go) is unwieldy.
+Options:
 
 - Split into per-profile files: `profiles_daily.go`, `profiles_git.go`, etc.
-- Or load default profiles from an embedded YAML file (`//go:embed defaults.yaml`) so
-  non-Go contributors can read and edit profiles without touching Go code.
+- Or load default profiles from an embedded YAML file (`//go:embed defaults.yaml`)
+  so non-Go contributors can read and edit profiles without touching Go code.
 
-The embedded YAML approach also makes it easy to write a single test that validates every
-default profile passes `validateProfile()`.
+The embedded YAML approach also makes it easy to write a single test that validates
+every default profile passes `validateProfile()`.
 
 ---
 
-## 7. Minor housekeeping
-
-- **Empty `src/` directory** — remove it (or document its purpose).
-- **`test-common.go`** — this file contains only a package declaration. Either add shared
-  test helpers to it or delete it.
-- **`dev-setup` script** — is gitignored but checked-in? Verify its contents don't contain
-  credentials and decide if it should be tracked.
-- **`.DS_Store`** — add `**/.DS_Store` to [.gitignore](.gitignore) to prevent future
-  commits of macOS metadata files.
-- **CI image pinning** — `.gitlab-ci.yml` uses `golang:1.25` (unpinned minor). Pin to a
-  digest or a patch tag (`golang:1.25.0`) for reproducible builds.
+## 8. Minor housekeeping
+
+- **Empty `src/` directory** — remove it.
+- **`.DS_Store`** — add `**/.DS_Store` to [.gitignore](.gitignore) to prevent
+  future commits of macOS metadata files.
+- **CI image pinning** — `.gitlab-ci.yml` uses `golang:1.25` (unpinned minor).
+  Pin to a patch tag (`golang:1.25.x`) for reproducible builds.
diff --git a/Makefile b/Makefile
index de0f88a..3caca2d 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ clean:
 	go mod tidy
 
 build: prep
-	go build ${LDFLAGS} -o ${BINARY} .
+	go build ${LDFLAGS} -o ${BINARY} ./cmd/template/
 
 lint: build tools-install
 	golangci-lint run --timeout=10m
@@ -39,7 +39,7 @@ test_coverage_html: test_coverage
 	open "./test-coverage.html"
 
 install: build test lint
-	go install ${LDFLAGS}
+	go install ${LDFLAGS} ./cmd/template/
 
 uninstall:
 	go clean -i gitlab.com/PavelSafronov/template...
diff --git a/go.mod b/go.mod
index 5b61547..c85ca10 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,6 @@ require (
 	github.com/sirupsen/logrus v1.9.4
 	github.com/stretchr/testify v1.11.1
 	github.com/urfave/cli/v2 v2.27.7
-	golang.org/x/exp v0.0.0-20260112195511-716be5621a96
 	gopkg.in/yaml.v3 v3.0.1
 )
 
diff --git a/go.sum b/go.sum
index 95224d3..6ce3423 100644
--- a/go.sum
+++ b/go.sum
@@ -44,8 +44,6 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAz
 github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
 golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
 golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
-golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
-golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
 golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
 golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/main.go b/internal/core/app.go
similarity index 59%
rename from main.go
rename to internal/core/app.go
index a7cc0b0..8abcd47 100644
--- a/main.go
+++ b/internal/core/app.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"bufio"
@@ -12,20 +12,15 @@ import (
 	"strings"
 
 	log "github.com/sirupsen/logrus"
-	"github.com/urfave/cli/v2" // imports as package "cli"
+	"github.com/urfave/cli/v2"
 	"gopkg.in/yaml.v3"
 )
 
-var (
-	// Version is the version of the application
-	Version string
-)
-
 const defaultUserConfigPath = "~/.template.user.yaml"
 const defaultConfigPath = "~/.template.yaml"
 const defaultLogLevel = "info"
 
-type ArgumentValues struct {
+type argumentValues struct {
 	profile        string
 	projectName    string
 	fileTemplate   string
@@ -36,20 +31,21 @@ type ArgumentValues struct {
 	extrasFilePath string
 	logLevel       string
 }
-type EnvVarValues struct {
+type envVarValues struct {
 	useDefaultConfig bool
 }
-type Settings struct {
-	ArgumentValues
-	EnvVarValues
+type settings struct {
+	argumentValues
+	envVarValues
 	extras         map[string]string
 	fullConfigPath string
 	config         Config
 	usr            *user.User
 }
 
-func main() {
-	argumentValues := ArgumentValues{}
+// Run is the entry point for the CLI application.
+func Run(args []string, version string) error {
+	av := argumentValues{}
 
 	app := &cli.App{
 		Name:                 "template",
@@ -62,66 +58,63 @@ func main() {
 				Aliases:     []string{"p"},
 				Value:       "",
 				Usage:       "`PROFILE` to use for the template",
-				Destination: &argumentValues.profile,
-				// Value:       defaultProfileName,
-				// Required:    true,
-				// EnvVars: []string{"TEMPLATE_PROFILE"},
+				Destination: &av.profile,
 			},
 			&cli.StringFlag{
 				Name:        "file",
 				Aliases:     []string{"f"},
 				Value:       "",
 				Usage:       "file template to use to create a file",
-				Destination: &argumentValues.fileTemplate,
+				Destination: &av.fileTemplate,
 			},
 			&cli.StringFlag{
 				Name:        "dir",
 				Aliases:     []string{"d"},
 				Value:       "",
 				Usage:       "folder pattern to use to create a directory",
-				Destination: &argumentValues.dirTemplate,
+				Destination: &av.dirTemplate,
 			},
 			&cli.StringFlag{
 				Name:        "script",
 				Aliases:     []string{"s"},
 				Value:       "",
 				Usage:       "templated script to run in the shell",
-				Destination: &argumentValues.scriptTemplate,
+				Destination: &av.scriptTemplate,
 			},
 			&cli.StringFlag{
 				Name:        "name",
 				Aliases:     []string{"n"},
 				Usage:       "name of the project to create",
 				Value:       "",
-				Destination: &argumentValues.projectName,
+				Destination: &av.projectName,
 			},
 			&cli.PathFlag{
 				Name:        "config-yaml",
 				Aliases:     []string{"c"},
 				Usage:       "path to the config yaml file",
 				Value:       defaultConfigPath,
-				Destination: &argumentValues.configFilePath,
+				Destination: &av.configFilePath,
 			},
 			&cli.PathFlag{
 				Name:        "user-config-yaml",
 				Aliases:     []string{"u"},
 				Usage:       "path to the user config yaml file",
 				Value:       defaultUserConfigPath,
-				Destination: &argumentValues.userConfigPath,
+				Destination: &av.userConfigPath,
 			},
 			&cli.PathFlag{
 				Name:        "extras-yaml",
 				Aliases:     []string{"e"},
 				Usage:       "path to the extras yaml file",
 				Value:       "~/.template-extras.yaml",
-				Destination: &argumentValues.extrasFilePath,
+				Destination: &av.extrasFilePath,
 			},
 			&cli.StringFlag{
 				Name:        "log-level",
 				Aliases:     []string{"l"},
 				Usage:       "log level",
 				Value:       defaultLogLevel,
-				Destination: &argumentValues.logLevel,
+				Destination: &av.logLevel,
 			},
 		},
 		Commands: []*cli.Command{
@@ -129,15 +122,14 @@ func main() {
 				Name:  "list",
 				Usage: "list available profiles",
 				Action: func(c *cli.Context) error {
-					settings, err := createSettings(argumentValues, c)
+					s, err := createSettings(av, c)
 					if err != nil {
 						return fmt.Errorf("error creating settings: %v", err)
 					}
 
-					config := settings.config
-					// list profiles
+					cfg := s.config
 					fmt.Println("Available profiles:")
-					for i, p := range config.Profiles {
+					for i, p := range cfg.Profiles {
 						fmt.Printf(" %d: %v\n", i+1, p.Name)
 						fmt.Printf("      %v\n", p.Description)
 					}
@@ -154,27 +146,26 @@ func main() {
 						Aliases:     []string{"d"},
 						Value:       "",
 						Usage:       "folder from which to create the template",
-						Destination: &argumentValues.dirTemplate,
+						Destination: &av.dirTemplate,
 					},
 					&cli.StringFlag{
 						Name:        "file",
 						Aliases:     []string{"f"},
 						Value:       "",
 						Usage:       "file from which to create the template",
-						Destination: &argumentValues.fileTemplate,
+						Destination: &av.fileTemplate,
 					},
 				},
 				Action: func(c *cli.Context) error {
-					// get args
 					c.Args()
 
-					filePath := argumentValues.fileTemplate
-					dirPath := argumentValues.dirTemplate
+					filePath := av.fileTemplate
+					dirPath := av.dirTemplate
 					if filePath == "" && dirPath == "" {
 						return fmt.Errorf("file or dir not specified, use `file` or `dir` flag")
 					}
 
-					profileName := argumentValues.profile
+					profileName := av.profile
 					if profileName == "" {
 						profileName = "temp"
 					}
@@ -202,39 +193,36 @@ func main() {
 					}
 
 					if filePath != "" {
-						file, err := readFile(filePath)
+						fileContent, err := readFile(filePath)
 						if err != nil {
 							return fmt.Errorf("error reading file %s: %w", filePath, err)
 						}
-						template := ParameterizedText(file)
-						profile.FileTemplate = template
+						profile.FileTemplate = ParameterizedText(fileContent)
 					} else {
-						files := []FileInFolder{}
-						err := filepath.WalkDir(dirPath, func(path string, d fs.DirEntry, err error) error {
+						var files []FileInFolder
+						err := filepath.WalkDir(dirPath, func(p string, d fs.DirEntry, err error) error {
 							if err != nil {
-								fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err)
+								fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", p, err)
 								return err
 							}
 
-							isRoot := path == dirPath
+							isRoot := p == dirPath
 							isDir := d.IsDir()
-							shouldSkip := isRoot || isDir
-							if shouldSkip {
+							if isRoot || isDir {
 								return nil
 							}
-							relPath, err := filepath.Rel(dirPath, path)
+							relPath, err := filepath.Rel(dirPath, p)
 							if err != nil {
-								return fmt.Errorf("error getting relative path for %s: %w", path, err)
+								return fmt.Errorf("error getting relative path for %s: %w", p, err)
 							}
-							contents, err := readFile(path)
+							contents, err := readFile(p)
 							if err != nil {
-								return fmt.Errorf("error reading file %s: %w", path, err)
+								return fmt.Errorf("error reading file %s: %w", p, err)
 							}
-							fif := FileInFolder{
+							files = append(files, FileInFolder{
 								Name:         ParameterizedText(relPath),
 								FileTemplate: ParameterizedText(contents),
-							}
-							files = append(files, fif)
+							})
 							return nil
 						})
 						if err != nil {
@@ -243,19 +231,13 @@ func main() {
 						profile.Files = files
 					}
 
-					config := Config{
-						Profiles: []Profile{profile},
-					}
-
+					cfg := Config{Profiles: []Profile{profile}}
 					writer := strings.Builder{}
 					encoder := yaml.NewEncoder(&writer)
-					err := encoder.Encode(config)
-					if err != nil {
+					if err := encoder.Encode(cfg); err != nil {
 						return fmt.Errorf("error encoding config: %v", err)
 					}
-
-					yamlText := writer.String()
-					fmt.Println(yamlText)
+					fmt.Println(writer.String())
 					return nil
 				},
 			},
@@ -263,29 +245,26 @@ func main() {
 				Name:  "get",
 				Usage: "get a profile",
 				Action: func(c *cli.Context) error {
-					settings, err := createSettings(argumentValues, c)
+					s, err := createSettings(av, c)
 					if err != nil {
 						return fmt.Errorf("error creating settings: %v", err)
 					}
-					config := settings.config
+					cfg := s.config
 
-					// get profile information
-					index, err := findProfileIndex(config.Profiles, settings.profile)
+					index, err := findProfileIndex(cfg.Profiles, s.profile)
 					if err != nil {
-						return fmt.Errorf("profile '%v' not found", settings.profile)
+						return fmt.Errorf("profile '%v' not found", s.profile)
 					}
-					profile := config.Profiles[index]
+					profile := cfg.Profiles[index]
 
 					writer := strings.Builder{}
 					encoder := yaml.NewEncoder(&writer)
-					err = encoder.Encode(profile)
-					if err != nil {
+					if err = encoder.Encode(profile); err != nil {
 						return fmt.Errorf("error encoding profile: %v", err)
 					}
 
-					yamlText := writer.String()
-					fmt.Printf("Profile '%v':\n", settings.profile)
-					fmt.Println(yamlText)
+					fmt.Printf("Profile '%v':\n", s.profile)
+					fmt.Println(writer.String())
 					fmt.Println()
 					return nil
 				},
@@ -298,7 +277,7 @@ func main() {
 					if err != nil {
 						log.Fatal(err)
 					}
-					fullConfigPath := getFullPath(argumentValues.configFilePath, *usr)
+					fullConfigPath := getFullPath(av.configFilePath, *usr)
 
 					_, err = os.Stat(fullConfigPath)
 					if err == nil {
@@ -308,8 +287,7 @@ func main() {
 					}
 
 					fmt.Printf("Setting config at '%v' to default\n", fullConfigPath)
-					err = saveConfig(fullConfigPath, defaultConfig())
-					if err != nil {
+					if err = saveConfig(fullConfigPath, defaultConfig()); err != nil {
 						return fmt.Errorf("error saving default config: %v", err)
 					}
 					fmt.Println()
@@ -324,11 +302,10 @@ func main() {
 					if err != nil {
 						log.Fatal(err)
 					}
-					fullConfigPath := getFullPath(argumentValues.configFilePath, *usr)
+					fullConfigPath := getFullPath(av.configFilePath, *usr)
 
 					fmt.Printf("Resetting config at '%v' to default\n", fullConfigPath)
-					err = saveConfig(fullConfigPath, defaultConfig())
-					if err != nil {
+					if err = saveConfig(fullConfigPath, defaultConfig()); err != nil {
 						return fmt.Errorf("error saving default config: %v", err)
 					}
 					fmt.Println()
@@ -339,19 +316,17 @@ func main() {
 				Name:  "version",
 				Usage: "print version",
 				Action: func(c *cli.Context) error {
-					fmt.Print(Version)
-					fmt.Println()
+					fmt.Println(version)
 					return nil
 				},
 			},
 		},
-		Action: func(context *cli.Context) error {
-			settings, err := createSettings(argumentValues, context)
+		Action: func(c *cli.Context) error {
+			s, err := createSettings(av, c)
 			if err != nil {
 				return fmt.Errorf("error parsing arguments: %w", err)
 			}
-			err = mainLogic(settings)
-			if err != nil {
+			if err = runMain(s); err != nil {
 				return fmt.Errorf("error in main logic: %w", err)
 			}
 			fmt.Println()
@@ -359,31 +334,24 @@ func main() {
 		},
 	}
 
-	if err := app.Run(os.Args); err != nil {
-		log.Fatal(err)
-	}
+	return app.Run(args)
 }
 
-func createSettings(
-	argumentValues ArgumentValues,
-	context *cli.Context,
-) (Settings, error) {
-	envVarValues, err := readEnvVars()
+func createSettings(av argumentValues, context *cli.Context) (settings, error) {
+	ev, err := readEnvVars()
 	if err != nil {
-		return Settings{}, fmt.Errorf("error getting env var values: %w", err)
+		return settings{}, fmt.Errorf("error getting env var values: %w", err)
 	}
 
-	settings := Settings{
-		ArgumentValues: argumentValues,
-		EnvVarValues:   envVarValues,
+	s := settings{
+		argumentValues: av,
+		envVarValues:   ev,
 	}
 
-	// read args
 	args := context.Args()
 
-	// configure logging
 	var level log.Level
-	switch strings.ToLower(settings.logLevel) {
+	switch strings.ToLower(s.logLevel) {
 	case "debug":
 		level = log.DebugLevel
 	case "info":
@@ -397,75 +365,65 @@ func createSettings(
 	case "panic":
 		level = log.PanicLevel
 	default:
-		return Settings{}, fmt.Errorf("invalid log level specified %v", settings.logLevel)
+		return settings{}, fmt.Errorf("invalid log level specified %v", s.logLevel)
 	}
 	log.SetLevel(level)
 	log.SetOutput(os.Stdout)
 
 	log.WithFields(log.Fields{"args": args}).Debug("Args parsed")
 
-	skipProfile := len(settings.fileTemplate) > 0 || len(settings.dirTemplate) > 0 || len(settings.scriptTemplate) > 0
+	skipProfile := len(s.fileTemplate) > 0 || len(s.dirTemplate) > 0 || len(s.scriptTemplate) > 0
 	currentParamIndex := 0
 
 	if !skipProfile {
-		// get profile name from args
-		// if not set explicitly, use first arg, if present
-		if settings.profile == "" && args.Len() > currentParamIndex {
-			settings.profile = args.Get(currentParamIndex)
+		if s.profile == "" && args.Len() > currentParamIndex {
+			s.profile = args.Get(currentParamIndex)
 		}
-		if settings.profile != "" {
-			fmt.Println("Profile:", settings.profile)
+		if s.profile != "" {
+			fmt.Println("Profile:", s.profile)
 		}
 		currentParamIndex++
 	}
 
-	// get project name from args
-	// if not set explicitly, use next arg (after profile), if present
-	if settings.projectName == "" && args.Len() > currentParamIndex {
-		settings.projectName = args.Get(currentParamIndex)
-		// currentParamIndex++
+	if s.projectName == "" && args.Len() > currentParamIndex {
+		s.projectName = args.Get(currentParamIndex)
 	}
-	if settings.projectName != "" {
-		fmt.Println("Project name:", settings.projectName)
+	if s.projectName != "" {
+		fmt.Println("Project name:", s.projectName)
 	}
 
-	// extras strings
-	_, err = os.Stat(settings.extrasFilePath)
+	_, err = os.Stat(s.extrasFilePath)
 	if err == nil {
-		extras, err := loadExtras(settings.extrasFilePath)
+		extras, err := loadExtras(s.extrasFilePath)
 		if err != nil {
-			return settings, fmt.Errorf("error loading extras from %v: %w", settings.extrasFilePath, err)
+			return s, fmt.Errorf("error loading extras from %v: %w", s.extrasFilePath, err)
 		}
-		settings.extras = extras
+		s.extras = extras
 	}
 
-	// get user
 	usr, err := user.Current()
 	if err != nil {
 		log.Fatal(err)
 	}
-	settings.usr = usr
+	s.usr = usr
 
-	// get config from default path
-	fullConfigPath := getFullPath(settings.configFilePath, *usr)
-	settings.fullConfigPath = fullConfigPath
+	fullConfigPath := getFullPath(s.configFilePath, *usr)
+	s.fullConfigPath = fullConfigPath
 
 	var fullConfig Config
-	if settings.useDefaultConfig {
+	if s.useDefaultConfig {
 		fullConfig = defaultConfig()
 	} else {
 		config, err := loadConfig(fullConfigPath)
 		if err != nil {
-			return settings, fmt.Errorf("error loading config from %v: %w\nConsider running `template reset` to recreate the config file, but keep in mind that `reset` will remove existing config settings", fullConfigPath, err)
+			return s, fmt.Errorf("error loading config from %v: %w\nConsider running `template reset` to recreate the config file, but keep in mind that `reset` will remove existing config settings", fullConfigPath, err)
 		}
 		fmt.Printf("Loaded config from %v\n", fullConfigPath)
-
-		// load user config, if present
-		fullConfig = loadAndMergeUserConfig(settings.userConfigPath, usr, config)
+		fullConfig = loadAndMergeUserConfig(s.userConfigPath, usr, config)
 	}
-	settings.config = fullConfig
+	s.config = fullConfig
 
-	return settings, nil
+	return s, nil
 }
 
 func loadAndMergeUserConfig(userConfigPath string, usr *user.User, globalConfig Config) Config {
@@ -491,45 +449,44 @@ func loadAndMergeUserConfig(userConfigPath string, usr *user.User, globalConfig
 	return fullConfig
 }
 
-func readEnvVars() (EnvVarValues, error) {
-	envVarValues := EnvVarValues{}
+func readEnvVars() (envVarValues, error) {
+	ev := envVarValues{}
 
 	useDefaultConfigText := os.Getenv("TEMPLATE_USE_DEFAULT_CONFIG")
 	if useDefaultConfigText != "" {
 		useDefaultConfig, err := strconv.ParseBool(useDefaultConfigText)
 		if err != nil {
-			return envVarValues, fmt.Errorf("error parsing env var TEMPLATE_USE_DEFAULT_CONFIG: \"%s\" %w", useDefaultConfigText, err)
+			return ev, fmt.Errorf("error parsing env var TEMPLATE_USE_DEFAULT_CONFIG: \"%s\" %w", useDefaultConfigText, err)
 		}
-		envVarValues.useDefaultConfig = useDefaultConfig
+		ev.useDefaultConfig = useDefaultConfig
 	}
 
-	return envVarValues, nil
+	return ev, nil
 }
 
-func findProfile(settings Settings) (Profile, error) {
-	var profile = Profile{}
-	var err error
-	config := settings.config
-	createTempProfile := len(settings.fileTemplate) > 0 || len(settings.dirTemplate) > 0 || len(settings.scriptTemplate) > 0
+func findProfile(s settings) (Profile, error) {
+	var profile Profile
+	cfg := s.config
+	createTempProfile := len(s.fileTemplate) > 0 || len(s.dirTemplate) > 0 || len(s.scriptTemplate) > 0
 
 	if createTempProfile {
-		if len(settings.scriptTemplate) > 0 {
+		if len(s.scriptTemplate) > 0 {
 			profile = Profile{
 				Name:        "temp",
 				Description: "temp-script-profile",
 				Type:        ScriptType,
-				PostScripts: []ParameterizedText{ParameterizedText(settings.scriptTemplate)},
+				PostScripts: []ParameterizedText{ParameterizedText(s.scriptTemplate)},
 			}
 		} else {
 			profileType := FileType
-			if len(settings.dirTemplate) > 0 {
+			if len(s.dirTemplate) > 0 {
 				profileType = FolderType
 			}
-			patterns := []ParameterizedText{}
-			if len(settings.fileTemplate) > 0 {
-				patterns = append(patterns, ParameterizedText(settings.fileTemplate))
-			} else if len(settings.dirTemplate) > 0 {
-				patterns = append(patterns, ParameterizedText(settings.dirTemplate))
+			var patterns []ParameterizedText
+			if len(s.fileTemplate) > 0 {
+				patterns = append(patterns, ParameterizedText(s.fileTemplate))
+			} else if len(s.dirTemplate) > 0 {
+				patterns = append(patterns, ParameterizedText(s.dirTemplate))
 			}
 			profile = Profile{
 				Name:         "temp",
@@ -539,25 +496,25 @@ func findProfile(settings Settings) (Profile, error) {
 			}
 		}
 	} else {
-		if settings.profile == "" {
-			profile, err = findDefaultProfile(config.Profiles)
+		if s.profile == "" {
+			var err error
+			profile, err = findDefaultProfile(cfg.Profiles)
 			if err != nil {
 				return profile, fmt.Errorf("no profile specified and no default profile found")
 			}
 		} else {
-			index, err := findProfileIndex(config.Profiles, settings.profile)
+			index, err := findProfileIndex(cfg.Profiles, s.profile)
 			if err != nil {
 				return profile, fmt.Errorf("error finding profile: %v", err)
 			}
-			profile = config.Profiles[index]
+			profile = cfg.Profiles[index]
 		}
 	}
 	return profile, nil
 }
 
-func mainLogic(settings Settings) error {
-	// find profile
-	profile, err := findProfile(settings)
+func runMain(s settings) error {
+	profile, err := findProfile(s)
 	if err != nil {
 		return fmt.Errorf("error finding profile: %v", err)
 	}
@@ -566,15 +523,14 @@ func mainLogic(settings Settings) error {
 	if err != nil {
 		return fmt.Errorf("error getting stdin stat: %w", err)
 	}
-	var pipeInputScanner *bufio.Scanner = nil
+	var pipeInputScanner *bufio.Scanner
 	incomingPipeData := (stat.Mode() & os.ModeCharDevice) == 0
 	if incomingPipeData {
 		pipeInputScanner = bufio.NewScanner(os.Stdin)
 		fmt.Printf("Reading from stdin\n")
 	}
 
-	// process profile
-	errors := processProfile(profile, settings.projectName, settings.extras, *settings.usr, pipeInputScanner)
+	errors := processProfile(profile, s.projectName, s.extras, *s.usr, pipeInputScanner)
 	if len(errors) > 0 {
 		return fmt.Errorf("error processing profile: %v", errors)
 	}
diff --git a/config.go b/internal/core/config.go
similarity index 98%
rename from config.go
rename to internal/core/config.go
index 2e12711..5dedb03 100644
--- a/config.go
+++ b/internal/core/config.go
@@ -1,10 +1,10 @@
-package main
+package core
 
 import (
 	"fmt"
 	"os"
 
-	"golang.org/x/exp/slices"
+	"slices"
 	"gopkg.in/yaml.v3"
 )
 
diff --git a/config_test.go b/internal/core/config_test.go
similarity index 99%
rename from config_test.go
rename to internal/core/config_test.go
index 3e1a45a..ed365d5 100644
--- a/config_test.go
+++ b/internal/core/config_test.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"errors"
diff --git a/default_config.go b/internal/core/default_config.go
similarity index 99%
rename from default_config.go
rename to internal/core/default_config.go
index df13d6f..e681d00 100644
--- a/default_config.go
+++ b/internal/core/default_config.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 func defaultConfig() Config {
 	config := Config{
diff --git a/defaultconfig_test.go b/internal/core/defaultconfig_test.go
similarity index 94%
rename from defaultconfig_test.go
rename to internal/core/defaultconfig_test.go
index b06ada8..7620381 100644
--- a/defaultconfig_test.go
+++ b/internal/core/defaultconfig_test.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"testing"
diff --git a/extras.go b/internal/core/extras.go
similarity index 96%
rename from extras.go
rename to internal/core/extras.go
index 018c0f6..88d5467 100644
--- a/extras.go
+++ b/internal/core/extras.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"fmt"
diff --git a/extras_test.go b/internal/core/extras_test.go
similarity index 98%
rename from extras_test.go
rename to internal/core/extras_test.go
index 96a72e5..6f80528 100644
--- a/extras_test.go
+++ b/internal/core/extras_test.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"os"
diff --git a/file.go b/internal/core/file.go
similarity index 95%
rename from file.go
rename to internal/core/file.go
index 54c2071..f88c287 100644
--- a/file.go
+++ b/internal/core/file.go
@@ -1,4 +1,4 @@
-package main

+package core
 
 import (
 	"bufio"
diff --git a/file_test.go b/internal/core/file_test.go
similarity index 99%
rename from file_test.go
rename to internal/core/file_test.go
index 7a75ed5..815bd72 100644
--- a/file_test.go
+++ b/internal/core/file_test.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"bufio"
diff --git a/main_test.go b/internal/core/main_test.go
similarity index 83%
rename from main_test.go
rename to internal/core/main_test.go
index ac1aa3f..5088ccf 100644
--- a/main_test.go
+++ b/internal/core/main_test.go
@@ -1,4 +1,4 @@
-package main

+package core

 
 import (
 	"os"
@@ -26,8 +26,8 @@ func shutdown() {
 
 func TestFindProfile(t *testing.T) {
 	fileTemplate := "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}.txt"
-	profile, err := findProfile(Settings{

-		ArgumentValues: ArgumentValues{

+	profile, err := findProfile(settings{

+		argumentValues: argumentValues{

 			fileTemplate: fileTemplate,
 		},
 	})
@@ -38,8 +38,8 @@ func TestFindProfile(t *testing.T) {
 	assert.Equal(t, ParameterizedText(fileTemplate), profile.NamePatterns[0])
 
 	dirTemplate := "{{.Year}}-{{.Month}}-{{.Day}}{{if .ProjectName}}-{{.ProjectName}}{{end}}"
-	profile, err = findProfile(Settings{

-		ArgumentValues: ArgumentValues{

+	profile, err = findProfile(settings{

+		argumentValues: argumentValues{

 			dirTemplate: dirTemplate,
 		},
 	})
diff --git a/monkey-patch.go b/internal/core/monkey-patch.go
similarity index 99%
rename from monkey-patch.go
rename to internal/core/monkey-patch.go
index 0ce2d18..bf593e9 100644
--- a/monkey-patch.go
+++ b/internal/core/monkey-patch.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"os"
diff --git a/name_pattern.go b/internal/core/name_pattern.go
similarity index 94%
rename from name_pattern.go
rename to internal/core/name_pattern.go
index da8db8e..b5cea6a 100644
--- a/name_pattern.go
+++ b/internal/core/name_pattern.go
@@ -1,4 +1,4 @@
-package main

+package core
 
 import (
 	"errors"
diff --git a/name_pattern_test.go b/internal/core/name_pattern_test.go
similarity index 96%
rename from name_pattern_test.go
rename to internal/core/name_pattern_test.go
index 9dfb76e..5a5af48 100644
--- a/name_pattern_test.go
+++ b/internal/core/name_pattern_test.go
@@ -1,11 +1,11 @@
-package main

+package core
 
 import (
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
-	"golang.org/x/exp/slices"

+	"slices"

 )
 
 // Mon Jan 2 15:04:05 MST 2006
diff --git a/parameter_data.go b/internal/core/parameter_data.go
similarity index 95%
rename from parameter_data.go
rename to internal/core/parameter_data.go
index 0f5c4d0..59f3d9a 100644
--- a/parameter_data.go
+++ b/internal/core/parameter_data.go
@@ -1,4 +1,4 @@
-package main

+package core
 
 import (
 	"path/filepath"
diff --git a/postscript.go b/internal/core/postscript.go
similarity index 93%
rename from postscript.go
rename to internal/core/postscript.go
index 4b072d1..f054fbe 100644
--- a/postscript.go
+++ b/internal/core/postscript.go
@@ -1,4 +1,4 @@
-package main

+package core
 
 import (
 	"fmt"
diff --git a/postscript_test.go b/internal/core/postscript_test.go
similarity index 95%
rename from postscript_test.go
rename to internal/core/postscript_test.go
index a1f9b98..4e91515 100644
--- a/postscript_test.go
+++ b/internal/core/postscript_test.go
@@ -1,4 +1,4 @@
-package main

+package core
 
 import (
 	"testing"
diff --git a/profile.go b/internal/core/profile.go
similarity index 99%
rename from profile.go
rename to internal/core/profile.go
index 01cf20a..7f28409 100644
--- a/profile.go
+++ b/internal/core/profile.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"bufio"
@@ -12,7 +12,7 @@ import (
 	"strings"
 	"time"
 
-	"golang.org/x/exp/slices"
+	"slices"
 )
 
 type ProfileType string
diff --git a/profile_test.go b/internal/core/profile_test.go
similarity index 99%
rename from profile_test.go
rename to internal/core/profile_test.go
index 86d3f70..52f5ed7 100644
--- a/profile_test.go
+++ b/internal/core/profile_test.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"io/fs"
diff --git a/templating.go b/internal/core/templating.go
similarity index 93%
rename from templating.go
rename to internal/core/templating.go
index ed70933..1fcf455 100644
--- a/templating.go
+++ b/internal/core/templating.go
@@ -1,4 +1,4 @@
-package main

+package core
 
 import (
 	"fmt"
diff --git a/test-common.go b/internal/core/test-common.go
similarity index 96%
rename from test-common.go
rename to internal/core/test-common.go
index dc7b324..20ec445 100644
--- a/test-common.go
+++ b/internal/core/test-common.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import "time"
 
diff --git a/utils.go b/internal/core/utils.go
similarity index 97%
rename from utils.go
rename to internal/core/utils.go
index eb774fc..26b847e 100644
--- a/utils.go
+++ b/internal/core/utils.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"os/user"
diff --git a/utils_test.go b/internal/core/utils_test.go
similarity index 98%
rename from utils_test.go
rename to internal/core/utils_test.go
index 851bdca..323adbd 100644
--- a/utils_test.go
+++ b/internal/core/utils_test.go
@@ -1,4 +1,4 @@
-package main
+package core
 
 import (
 	"os/user"