feat: add scaffolder
This commit is contained in:
commit
01023c212b
2
.drone.yml
Normal file
2
.drone.yml
Normal file
@ -0,0 +1,2 @@
|
||||
kind: template
|
||||
load: cuddle-empty-plan.yaml
|
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
.settings
|
||||
.git
|
||||
.idea
|
||||
*.iml
|
||||
.vscode
|
||||
.DS_Store
|
||||
reports/*
|
||||
vendor
|
||||
buildInfo.json
|
||||
/.shuttle/
|
||||
/.dockerignore
|
||||
/main
|
||||
/local.env
|
||||
env.local
|
||||
|
||||
registry/*/testdata/*/actual/
|
41
README.md
Normal file
41
README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Scaffold
|
||||
|
||||
Scaffold is a cli that allows Developer to easily scaffold (create or update files) according to best practices.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
scaffold
|
||||
> Pick a template
|
||||
> Fill required information for the template
|
||||
> Profit
|
||||
```
|
||||
|
||||
Optionally if you know what you're looking for:
|
||||
|
||||
```bash
|
||||
scaffold externalhttp # --package app as an example
|
||||
```
|
||||
|
||||
Scaffold allows a wide variety of formatting options, such as template defined inputs, where the files should be placed, if they should be overwritten or not.
|
||||
|
||||
## Develop your own template
|
||||
|
||||
Templates are maintained in the `registry` folder. This is automatically kept up-to-date by the `scaffold`, in the folder you will see all the available templates.
|
||||
|
||||
To develop your own
|
||||
|
||||
```
|
||||
go run ./main.go --registry registry scaffold --name your_scaffold_here
|
||||
```
|
||||
|
||||
Scaffold will now have created a sample scaffold in the `registry/your_scaffold_here` folder along with tests.
|
||||
|
||||
A template consists of the following files:
|
||||
|
||||
- `scaffold.yaml`: Controls how the scaffold is supposed to work, which inputs it has, etc.
|
||||
- `scaffold_test.go`: Optional, but recommended tests which runs a set of input on the template and checks the output
|
||||
- `files/*.gotmpl`: Files to be scaffolded, it is recommended to provide a suffix of `.gotmpl` to the files, especially for golang files, which might otherwise mess with the project. Each file is templated using go templates, and is put in the path specified by the user, or via. the default path from the scaffold file. Files can also be put in directories, and the folder structure will be preserved.
|
||||
- `testdata/your_test_here/actual && expected`: contains tests to match the output of the scaffolder. This is especially useful to test a variety of input, for example using the defaults, vs. getting other info from the user.
|
||||
|
||||
To make your plugin available, simply create a pr on this repository, merge and your users should have it available shortly
|
96
cmd/new.go
Normal file
96
cmd/new.go
Normal file
@ -0,0 +1,96 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/templates"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func getScaffoldCommands(registryPath *string) ([]*cobra.Command, error) {
|
||||
var (
|
||||
ctx = context.Background()
|
||||
ui = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{}))
|
||||
templateIndexer = templates.NewTemplateIndexer()
|
||||
templateLoader = templates.NewTemplateLoader()
|
||||
fileWriter = templates.NewFileWriter().WithPromptOverride(promptOverrideFile)
|
||||
)
|
||||
|
||||
templateFiles, err := templateIndexer.Index(ctx, *registryPath, ui)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to index templates: %w", err)
|
||||
}
|
||||
|
||||
commands := make([]*cobra.Command, 0)
|
||||
for _, template := range templateFiles {
|
||||
var templatePath string
|
||||
variables := make([]*LazyVariable, 0)
|
||||
|
||||
for name, variable := range template.File.Input {
|
||||
variables = append(variables, &LazyVariable{
|
||||
Name: name,
|
||||
Description: variable.Description,
|
||||
Value: variable.Default,
|
||||
})
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: template.File.Name,
|
||||
SilenceUsage: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ui.Info("Loading template files", "name", template.File.Name)
|
||||
|
||||
for _, variable := range variables {
|
||||
ui.Info("found value", "key", variable.Name, "value", variable.Value)
|
||||
template.Input[variable.Name] = variable.Value
|
||||
}
|
||||
|
||||
if templatePath == "" {
|
||||
scaffoldDest, err := templates.TemplatePath(&template)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templatePath = scaffoldDest
|
||||
}
|
||||
|
||||
files, err := templateLoader.Load(ctx, &template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load template files: %w", err)
|
||||
}
|
||||
|
||||
templatedFiles, err := templateLoader.TemplateFiles(&template, files, templatePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to template files: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("Templated files", "files", len(templatedFiles))
|
||||
|
||||
if err := fileWriter.Write(ctx, ui, templatedFiles); err != nil {
|
||||
return fmt.Errorf("failed to write files: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&templatePath, "path", "", "which path to put the output files")
|
||||
|
||||
for _, variable := range variables {
|
||||
cmd.Flags().StringVar(&variable.Value, variable.Name, variable.Value, variable.Description)
|
||||
}
|
||||
|
||||
commands = append(commands, cmd)
|
||||
}
|
||||
|
||||
return commands, nil
|
||||
}
|
||||
|
||||
type LazyVariable struct {
|
||||
Name string
|
||||
Description string
|
||||
Value string
|
||||
}
|
241
cmd/root.go
Normal file
241
cmd/root.go
Normal file
@ -0,0 +1,241 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/ktr0731/go-fuzzyfinder"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/fetcher"
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/templates"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
var registryPath string
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "scaffold",
|
||||
Short: "pick a template, and scaffold a piece of code",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runScaffold(cmd.Context(), ®istryPath); err != nil {
|
||||
fmt.Printf("failed to run scaffold: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().StringVar(®istryPath, "registry", "", "where to get the registry from, defaults to upstream repository")
|
||||
_ = rootCmd.ParseFlags(os.Args)
|
||||
|
||||
subCommands, err := getScaffoldCommands(®istryPath)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to setup subcommands: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(subCommands) > 0 {
|
||||
rootCmd.AddCommand(subCommands...)
|
||||
}
|
||||
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func runScaffold(ctx context.Context, registryPath *string) error {
|
||||
ui := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
fetcher := fetcher.NewFetcher()
|
||||
templateIndexer := templates.NewTemplateIndexer()
|
||||
templateLoader := templates.NewTemplateLoader()
|
||||
fileWriter := templates.NewFileWriter().WithPromptOverride(promptOverrideFile)
|
||||
|
||||
if err := fetcher.CloneRepository(ctx, registryPath, ui); err != nil {
|
||||
return fmt.Errorf("failed to clone repository: %w", err)
|
||||
}
|
||||
|
||||
templates, err := templateIndexer.Index(ctx, *registryPath, ui)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to index templates: %w", err)
|
||||
}
|
||||
|
||||
template, err := chooseTemplate(templates)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to choose a template: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("Loading template files", "name", template.File.Name)
|
||||
|
||||
files, err := templateLoader.Load(ctx, template)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load template files: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("Loaded templates", "files", len(files))
|
||||
|
||||
scaffoldDest, err := promptInput(template, files)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prompt input: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("Templating files")
|
||||
|
||||
templatedFiles, err := templateLoader.TemplateFiles(template, files, scaffoldDest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to template files: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("Templated files", "files", len(templatedFiles))
|
||||
|
||||
if err := fileWriter.Write(ctx, ui, templatedFiles); err != nil {
|
||||
return fmt.Errorf("failed to write files: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func promptOverrideFile(file templates.TemplatedFile) (bool, error) {
|
||||
theme := huh.ThemeBase16()
|
||||
theme.FieldSeparator = lipgloss.NewStyle().SetString("\n")
|
||||
theme.Help.FullKey.MarginTop(1)
|
||||
|
||||
confirm := false
|
||||
f := huh.
|
||||
NewForm(
|
||||
huh.
|
||||
NewGroup(
|
||||
huh.
|
||||
NewConfirm().
|
||||
Title(fmt.Sprintf("Should override existing file?: %s", file.DestinationPath)).
|
||||
Value(&confirm),
|
||||
),
|
||||
).
|
||||
WithTheme(theme)
|
||||
err := f.Run()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to specify path for scaffold: %w", err)
|
||||
}
|
||||
|
||||
return confirm, nil
|
||||
}
|
||||
|
||||
func promptInput(template *templates.Template, files []templates.File) (string, error) {
|
||||
if len(template.File.Input) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
theme := huh.ThemeBase16()
|
||||
theme.FieldSeparator = lipgloss.NewStyle().SetString("\n")
|
||||
theme.Help.FullKey.MarginTop(1)
|
||||
|
||||
for input, inputSpec := range template.File.Input {
|
||||
inputVal := inputSpec.Default
|
||||
f := huh.
|
||||
NewForm(
|
||||
huh.
|
||||
NewGroup(
|
||||
huh.
|
||||
NewText().
|
||||
TitleFunc(
|
||||
func() string {
|
||||
return fmt.Sprintf("Template requires: %s", input)
|
||||
},
|
||||
&inputVal,
|
||||
).
|
||||
Value(&inputVal).
|
||||
Description(inputSpec.Description).
|
||||
WithHeight(1),
|
||||
),
|
||||
).
|
||||
WithTheme(theme)
|
||||
|
||||
err := f.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find template variable: %s, %w", input, err)
|
||||
}
|
||||
|
||||
if inputSpec.Type == "int" {
|
||||
if _, err := strconv.Atoi(inputVal); err != nil {
|
||||
return "", fmt.Errorf("input: '%s' for variable: '%s' is not an int: \n%w", inputVal, input, err)
|
||||
}
|
||||
}
|
||||
|
||||
template.Input[input] = inputVal
|
||||
}
|
||||
|
||||
scaffoldDest, err := templates.TemplatePath(template)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to template path: %w", err)
|
||||
}
|
||||
f := huh.
|
||||
NewForm(
|
||||
huh.
|
||||
NewGroup(
|
||||
huh.
|
||||
NewText().
|
||||
Title("Path: where to scaffold files: %s").
|
||||
Value(&scaffoldDest).
|
||||
DescriptionFunc(func() string {
|
||||
var sb strings.Builder
|
||||
|
||||
_, err := sb.WriteString("Preview of file paths:\n")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
previewFilePath := path.Join(scaffoldDest, file.RelPath)
|
||||
|
||||
if _, err := sb.WriteString(fmt.Sprintf("%s\n", previewFilePath)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}, &scaffoldDest),
|
||||
),
|
||||
).
|
||||
WithTheme(theme)
|
||||
err = f.Run()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to specify path for scaffold: %w", err)
|
||||
}
|
||||
|
||||
if scaffoldDest == "" {
|
||||
return "", errors.New("path cannot be an empty string")
|
||||
}
|
||||
|
||||
return scaffoldDest, nil
|
||||
}
|
||||
|
||||
func chooseTemplate(templates []templates.Template) (*templates.Template, error) {
|
||||
idx, err := fuzzyfinder.Find(
|
||||
templates,
|
||||
func(i int) string {
|
||||
return templates[i].File.Name
|
||||
},
|
||||
fuzzyfinder.WithPreviewWindow(func(i, w, h int) string {
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
|
||||
template := templates[i]
|
||||
templateContent, err := yaml.Marshal(template.File)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("failed to format template: %s", err.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Template:\n===\n%s\n===\n", string(templateContent))
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find a template: %w", err)
|
||||
}
|
||||
|
||||
return &templates[idx], nil
|
||||
}
|
16
cuddle.yaml
Normal file
16
cuddle.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
# yaml-language-server: $schema=https://git.front.kjuulh.io/kjuulh/cuddle/raw/branch/main/schemas/base.json
|
||||
|
||||
base: "git@git.front.kjuulh.io:kjuulh/cuddle-empty-plan.git"
|
||||
|
||||
vars:
|
||||
service: "scaffold"
|
||||
registry: kasperhermansen
|
||||
|
||||
please:
|
||||
project:
|
||||
owner: kjuulh
|
||||
repository: "scaffold"
|
||||
branch: main
|
||||
settings:
|
||||
api_url: https://git.front.kjuulh.io
|
||||
|
74
go.mod
Normal file
74
go.mod
Normal file
@ -0,0 +1,74 @@
|
||||
module git.front.kjuulh.io/kjuulh/scaffold
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/huh v0.6.0
|
||||
github.com/charmbracelet/lipgloss v1.0.0
|
||||
github.com/ktr0731/go-fuzzyfinder v0.8.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
git.front.kjuulh.io/kjuulh/scaffoldhttp v1.9.0
|
||||
git.front.kjuulh.io/kjuulh/scaffoldui-logger v0.1.0
|
||||
golang.org/x/sync v0.11.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.20.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.4.2 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/gdamore/encoding v1.0.0 // indirect
|
||||
github.com/gdamore/tcell/v2 v2.6.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/ktr0731/go-ansisgr v0.1.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/nsf/termbox-go v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
git.front.kjuulh.io/kjuulh/scaffoldcorrelation v1.0.1 // indirect
|
||||
git.front.kjuulh.io/kjuulh/scaffoldzlog v1.3.2 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 // indirect
|
||||
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
go.uber.org/zap/exp v0.2.0 // indirect
|
||||
golang.org/x/sys v0.25.0 // indirect
|
||||
golang.org/x/term v0.5.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
189
go.sum
Normal file
189
go.sum
Normal file
@ -0,0 +1,189 @@
|
||||
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
|
||||
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
|
||||
github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
|
||||
github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
|
||||
github.com/charmbracelet/huh v0.6.0 h1:mZM8VvZGuE0hoDXq6XLxRtgfWyTI3b2jZNKh0xWmax8=
|
||||
github.com/charmbracelet/huh v0.6.0/go.mod h1:GGNKeWCeNzKpEOh/OJD8WBwTQjV3prFAtQPpLv+AVwU=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk=
|
||||
github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
|
||||
github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg=
|
||||
github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
||||
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
|
||||
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
|
||||
github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/ktr0731/go-ansisgr v0.1.0 h1:fbuupput8739hQbEmZn1cEKjqQFwtCCZNznnF6ANo5w=
|
||||
github.com/ktr0731/go-ansisgr v0.1.0/go.mod h1:G9lxwgBwH0iey0Dw5YQd7n6PmQTwTuTM/X5Sgm/UrzE=
|
||||
github.com/ktr0731/go-fuzzyfinder v0.8.0 h1:+yobwo9lqZZ7jd1URPdCgZXTE2U1mpIVTkQoo4roi6w=
|
||||
github.com/ktr0731/go-fuzzyfinder v0.8.0/go.mod h1:Bjpz5im+tppKE9Ii6UK1h+6RaX/lUvJ0ruO4LIYRkqo=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a h1:2MaM6YC3mGu54x+RKAA6JiFFHlHDY1UbkxqppT7wYOg=
|
||||
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a/go.mod h1:hxSnBBYLK21Vtq/PHd0S2FYCxBXzBua8ov5s1RobyRQ=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY=
|
||||
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldcorrelation v1.0.1 h1:6cZ+bbliY1xtXYoWom+p/vBWpGcASHxk0TOsruzOEmE=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldcorrelation v1.0.1/go.mod h1:isjTaeBzq9bqFopUiDThuy4ts3wOMC4MWEnyKBAru7E=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldhttp v1.9.0 h1:QUXeztzoE/sX36iRaC26RWdCd5bhRAJ9pPqiOiliObI=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldhttp v1.9.0/go.mod h1:1nc+PsFOW68wf8AwkrL4F58zXFB/lEFCuMn0jK3FqNs=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldui-logger v0.1.0 h1:xvoi3lSoHR7wmIBLmU7TiLc8NZ2EnDP6pl250/vyMCo=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldui-logger v0.1.0/go.mod h1:wiXZzc0VZBXByAgNHQB67HWdHdaVhC+EA+BC0OcM4q0=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldzlog v1.3.2 h1:SC2aMj9rLpSIwa/IS57PDjFWELqcYhFgct6qNtkZUdU=
|
||||
git.front.kjuulh.io/kjuulh/scaffoldzlog v1.3.2/go.mod h1:gmLlCoBM2FHYOg7TJM/i9abwU9Q/owAztCbbqkeyEPs=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0 h1:DheMAlT6POBP+gh8RUH19EOTnQIor5QE0uSRPtzCpSw=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.57.0/go.mod h1:wZcGmeVO9nzP67aYSLDqXNWK87EZWhi7JWj1v7ZXf94=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
|
||||
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
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=
|
130
internal/fetcher/fetcher.go
Normal file
130
internal/fetcher/fetcher.go
Normal file
@ -0,0 +1,130 @@
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Fetcher allows pulling from an upstream scaffold registry. This is hard coded to the lunarway/scaffold registry, it can also be provided by a path which in that case, will not do anything
|
||||
type Fetcher struct{}
|
||||
|
||||
func NewFetcher() *Fetcher {
|
||||
return &Fetcher{}
|
||||
}
|
||||
|
||||
const readWriteExec = 0o644
|
||||
|
||||
const githubProject = "kjuulh/scaffold"
|
||||
|
||||
var (
|
||||
scaffoldFolder = os.ExpandEnv("$HOME/.scaffold")
|
||||
scaffoldClone = path.Join(scaffoldFolder, "upstream")
|
||||
scaffoldCache = path.Join(scaffoldFolder, "scaffold.updates.json")
|
||||
)
|
||||
|
||||
func (f *Fetcher) CloneRepository(ctx context.Context, registryPath *string, ui *slog.Logger) error {
|
||||
if err := os.MkdirAll(scaffoldFolder, readWriteExec); err != nil {
|
||||
return fmt.Errorf("failed to create scaffold folder: %w", err)
|
||||
}
|
||||
|
||||
if *registryPath == "" {
|
||||
if _, err := os.Stat(scaffoldClone); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to find the upstream folder: %w", err)
|
||||
}
|
||||
|
||||
ui.Info("cloning upstream templates")
|
||||
if err := cloneUpstream(ctx); err != nil {
|
||||
return fmt.Errorf("failed to clone upstream registry: %w", err)
|
||||
}
|
||||
} else {
|
||||
now := time.Now()
|
||||
lastUpdatedUnix := getCacheUpdate(ui, ctx)
|
||||
lastUpdated := time.Unix(lastUpdatedUnix, 0)
|
||||
|
||||
// Cache for 7 days
|
||||
if lastUpdated.Before(now.Add(-time.Hour * 24 * 7)) {
|
||||
ui.Info("update templates folder")
|
||||
if err := f.UpdateUpstream(ctx); err != nil {
|
||||
return fmt.Errorf("failed to update upstream scaffold folder: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Fetcher) UpdateUpstream(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "git", "pull", "--rebase")
|
||||
cmd.Dir = scaffoldClone
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("git pull failed with output: %s\n\n", string(output))
|
||||
return fmt.Errorf("git pull failed: %w", err)
|
||||
}
|
||||
|
||||
if err := createCacheUpdate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cloneUpstream(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "coffee", "repo", "clone", githubProject, scaffoldClone)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("git clone failed with output: %s\n\n", string(output))
|
||||
return fmt.Errorf("git clone failed: %w", err)
|
||||
}
|
||||
|
||||
if err := createCacheUpdate(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type CacheUpdate struct {
|
||||
LastUpdated int64 `json:"lastUpdated"`
|
||||
}
|
||||
|
||||
func createCacheUpdate(_ context.Context) error {
|
||||
content, err := json.Marshal(CacheUpdate{
|
||||
LastUpdated: time.Now().Unix(),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to prepare cache update: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(scaffoldCache, content, readWriteExec); err != nil {
|
||||
return fmt.Errorf("failed to write cache update: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getCacheUpdate(ui *slog.Logger, _ context.Context) int64 {
|
||||
content, err := os.ReadFile(scaffoldCache)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var cacheUpdate CacheUpdate
|
||||
if err := json.Unmarshal(content, &cacheUpdate); err != nil {
|
||||
ui.Warn("failed to read cache, it might be invalid", "error", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return cacheUpdate.LastUpdated
|
||||
}
|
163
internal/templates/loader.go
Normal file
163
internal/templates/loader.go
Normal file
@ -0,0 +1,163 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
gotmpl "text/template"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// TemplateLoader reads a templates files and runs their respective templating on them.
|
||||
type TemplateLoader struct{}
|
||||
|
||||
func NewTemplateLoader() *TemplateLoader {
|
||||
return &TemplateLoader{}
|
||||
}
|
||||
|
||||
type File struct {
|
||||
content []byte
|
||||
path string
|
||||
RelPath string
|
||||
}
|
||||
|
||||
var funcs = gotmpl.FuncMap{
|
||||
"ReplaceAll": strings.ReplaceAll,
|
||||
"ToLower": strings.ToLower,
|
||||
"ToUpper": strings.ToUpper,
|
||||
}
|
||||
|
||||
// TemplatePath formats the template file path using go templates, this is useful for programmatically changing the output string using go tmpls
|
||||
func TemplatePath(template *Template) (string, error) {
|
||||
tmpl, err := gotmpl.New("path").Funcs(funcs).Parse(template.File.Default.Path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
if err := tmpl.Execute(output, template); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
templatePath := strings.TrimSpace(output.String())
|
||||
|
||||
return templatePath, nil
|
||||
}
|
||||
|
||||
// Load loads the template files from disk
|
||||
func (t *TemplateLoader) Load(ctx context.Context, template *Template) ([]File, error) {
|
||||
templateFilePath := path.Join(template.Path, "files")
|
||||
if _, err := os.Stat(templateFilePath); err != nil {
|
||||
return nil, fmt.Errorf("failed to lookup template files %s, %w", templateFilePath, err)
|
||||
}
|
||||
|
||||
filePaths := make([]string, 0)
|
||||
err := filepath.WalkDir(templateFilePath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Is a file
|
||||
if d.Type().IsRegular() {
|
||||
filePaths = append(filePaths, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read template files: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
filesLock sync.Mutex
|
||||
files = make([]File, 0)
|
||||
)
|
||||
egrp, _ := errgroup.WithContext(ctx)
|
||||
for _, filePath := range filePaths {
|
||||
egrp.Go(func() error {
|
||||
fileContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file: %s, %w", filePath, err)
|
||||
}
|
||||
|
||||
filesLock.Lock()
|
||||
defer filesLock.Unlock()
|
||||
|
||||
files = append(files, File{
|
||||
content: fileContent,
|
||||
path: filePath,
|
||||
RelPath: strings.TrimPrefix(strings.TrimPrefix(filePath, templateFilePath), "/"),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
}
|
||||
if err := egrp.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
type TemplatedFile struct {
|
||||
Content []byte
|
||||
DestinationPath string
|
||||
}
|
||||
|
||||
// TemplateFiles runs the actual templating on the files, and tells it where to go. The writes doesn't happen here yet.
|
||||
func (l *TemplateLoader) TemplateFiles(template *Template, files []File, scaffoldDest string) ([]TemplatedFile, error) {
|
||||
templatedFiles := make([]TemplatedFile, 0)
|
||||
for _, file := range files {
|
||||
tmpl, err := gotmpl.
|
||||
New(file.RelPath).
|
||||
Funcs(funcs).
|
||||
Parse(string(file.content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse template file: %s, %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
if err := tmpl.Execute(output, template); err != nil {
|
||||
return nil, fmt.Errorf("failed to write template file: %s, %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
fileDir := path.Dir(file.RelPath)
|
||||
fileName := strings.TrimSuffix(path.Base(file.RelPath), ".gotmpl")
|
||||
if fileConfig, ok := template.File.Files[strings.TrimSuffix(file.RelPath, ".gotmpl")]; ok && fileConfig.Rename != "" {
|
||||
renameTmpl, err := gotmpl.New(file.RelPath).Funcs(funcs).Parse(fileConfig.Rename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse rename for: %s in scaffold.yaml: %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
type RenameContext struct {
|
||||
Template
|
||||
OriginalFileName string
|
||||
}
|
||||
|
||||
output := bytes.NewBufferString("")
|
||||
if err := renameTmpl.Execute(output, RenameContext{
|
||||
Template: *template,
|
||||
OriginalFileName: fileName,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("failed to template rename: %s, %w", file.RelPath, err)
|
||||
}
|
||||
|
||||
fileName = strings.TrimSpace(output.String())
|
||||
}
|
||||
|
||||
templatedFiles = append(templatedFiles, TemplatedFile{
|
||||
Content: output.Bytes(),
|
||||
DestinationPath: path.Join(scaffoldDest, fileDir, fileName),
|
||||
})
|
||||
}
|
||||
|
||||
return templatedFiles, nil
|
||||
}
|
97
internal/templates/templates.go
Normal file
97
internal/templates/templates.go
Normal file
@ -0,0 +1,97 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// TemplateIndexer loads all template specifications from the registry, allowing the caller to choose on of them, or simply display their properties.
|
||||
type TemplateIndexer struct{}
|
||||
|
||||
func NewTemplateIndexer() *TemplateIndexer {
|
||||
return &TemplateIndexer{}
|
||||
}
|
||||
|
||||
type Template struct {
|
||||
File TemplateFile
|
||||
Path string
|
||||
|
||||
Input map[string]string
|
||||
}
|
||||
|
||||
type TemplateDefault struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
type TemplateInputs map[string]TemplateInput
|
||||
|
||||
type TemplateInput struct {
|
||||
Type string `yaml:"type"`
|
||||
Description string `yaml:"description"`
|
||||
Default string `yaml:"default"`
|
||||
}
|
||||
|
||||
type TemplateFileConfig struct {
|
||||
Rename string `yaml:"rename"`
|
||||
}
|
||||
|
||||
type TemplateFile struct {
|
||||
Name string `yaml:"name"`
|
||||
Default TemplateDefault `yaml:"default"`
|
||||
Input TemplateInputs `yaml:"input"`
|
||||
Files map[string]TemplateFileConfig
|
||||
}
|
||||
|
||||
func (t *TemplateIndexer) Index(ctx context.Context, scaffoldRegistryFolder string, ui *slog.Logger) ([]Template, error) {
|
||||
ui.Debug("Loading templates...")
|
||||
|
||||
templateDirEntries, err := os.ReadDir(scaffoldRegistryFolder)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read templates dir: %w", err)
|
||||
}
|
||||
|
||||
var (
|
||||
templates = make([]Template, 0)
|
||||
templatesLock sync.Mutex
|
||||
)
|
||||
egrp, _ := errgroup.WithContext(ctx)
|
||||
for _, templateDirEntry := range templateDirEntries {
|
||||
egrp.Go(func() error {
|
||||
templatePath := path.Join(scaffoldRegistryFolder, templateDirEntry.Name())
|
||||
|
||||
content, err := os.ReadFile(path.Join(templatePath, "scaffold.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read: %s, %w", templateDirEntry.Name(), err)
|
||||
}
|
||||
|
||||
var template TemplateFile
|
||||
if err := yaml.Unmarshal(content, &template); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal template: %s, %w", string(content), err)
|
||||
}
|
||||
|
||||
templatesLock.Lock()
|
||||
defer templatesLock.Unlock()
|
||||
templates = append(templates, Template{
|
||||
File: template,
|
||||
Path: templatePath,
|
||||
Input: make(map[string]string),
|
||||
})
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := egrp.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ui.Debug("Done loading templates...", "amount", len(templates))
|
||||
|
||||
return templates, nil
|
||||
}
|
84
internal/templates/writer.go
Normal file
84
internal/templates/writer.go
Normal file
@ -0,0 +1,84 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const readWriteExec = 0o755
|
||||
const readExec = 0o644
|
||||
|
||||
type PromptOverride func(file TemplatedFile) (bool, error)
|
||||
|
||||
// FileWriter writes the actual files to disk, it optionally takes a promptOverride which allows the caller to stop a potential override of a file
|
||||
type FileWriter struct {
|
||||
promptOverride PromptOverride
|
||||
}
|
||||
|
||||
func NewFileWriter() *FileWriter {
|
||||
return &FileWriter{
|
||||
promptOverride: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileWriter) WithPromptOverride(po PromptOverride) *FileWriter {
|
||||
f.promptOverride = po
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *FileWriter) Write(ctx context.Context, ui *slog.Logger, templatedFiles []TemplatedFile) error {
|
||||
var fileExistsLock sync.Mutex
|
||||
|
||||
egrp, _ := errgroup.WithContext(ctx)
|
||||
for _, file := range templatedFiles {
|
||||
egrp.Go(func() error {
|
||||
if _, err := os.Stat(file.DestinationPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to check if file exists: %s, %w", file.DestinationPath, err)
|
||||
}
|
||||
} else {
|
||||
if f.promptOverride != nil {
|
||||
fileExistsLock.Lock()
|
||||
defer fileExistsLock.Unlock()
|
||||
|
||||
override, err := f.promptOverride(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get answer to whether a file should be overwritten or not: %w", err)
|
||||
}
|
||||
|
||||
if !override {
|
||||
ui.Warn("Skipping file", "file", file.DestinationPath)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parent := path.Dir(file.DestinationPath); parent != "" && parent != "/" {
|
||||
if err := os.MkdirAll(parent, readWriteExec); err != nil {
|
||||
return fmt.Errorf("failed to create parent dir for: %s, %w", file.DestinationPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
ui.Info("writing file", "path", file.DestinationPath)
|
||||
if err := os.WriteFile(file.DestinationPath, file.Content, readExec); err != nil {
|
||||
return fmt.Errorf("failed to write file: %s, %w", file.DestinationPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := egrp.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
206
internal/tests/fixture.go
Normal file
206
internal/tests/fixture.go
Normal file
@ -0,0 +1,206 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/templates"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ScaffoldFixture provides an api on top of the scaffold templater, this is opposed to calling the cli
|
||||
type ScaffoldFixture struct {
|
||||
vars map[string]string
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *ScaffoldFixture) WithVariable(key, val string) *ScaffoldFixture {
|
||||
s.vars[key] = val
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *ScaffoldFixture) WithPath(path string) *ScaffoldFixture {
|
||||
s.path = path
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// TestFixture is an opinionated way for the templates to be able to test their code, this also works as an accepttest for the scaffolder itself.
|
||||
type TestFixture struct {
|
||||
pkg string
|
||||
t *testing.T
|
||||
}
|
||||
|
||||
func Test(t *testing.T, pkg string) *TestFixture {
|
||||
return &TestFixture{
|
||||
pkg: pkg,
|
||||
t: t,
|
||||
}
|
||||
}
|
||||
|
||||
// ScaffoldDefaultTest tests that the code can be run with the default variables. We want to have sane defaults for most things. As such there is an opinionated way of running these tests
|
||||
func (f *TestFixture) ScaffoldDefaultTest(testName string) *TestFixture {
|
||||
f.ScaffoldTest(testName, func(fixture *ScaffoldFixture) {})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
// ScaffoldTest is a large fixture, which allows running the accepttest over a template, in turn creating files and comparing them. either way they have to match, if the templater generated more files we expect, it fails, if they're different the test fails
|
||||
func (f *TestFixture) ScaffoldTest(testName string, input func(fixture *ScaffoldFixture)) *TestFixture {
|
||||
f.t.Run(
|
||||
f.pkg,
|
||||
func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testName := strings.ToLower(strings.ReplaceAll(testName, " ", "_"))
|
||||
|
||||
fixture := &ScaffoldFixture{
|
||||
vars: make(map[string]string),
|
||||
}
|
||||
input(fixture)
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
indexer := templates.NewTemplateIndexer()
|
||||
loader := templates.NewTemplateLoader()
|
||||
writer := templates.NewFileWriter()
|
||||
ui := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
||||
|
||||
templateFiles, err := indexer.Index(ctx, "../", ui)
|
||||
require.NoError(t, err)
|
||||
|
||||
template, err := find(templateFiles, f.pkg)
|
||||
require.NoError(t, err, "failed to find a template")
|
||||
|
||||
files, err := loader.Load(ctx, template)
|
||||
require.NoError(t, err, "failed to load template files")
|
||||
|
||||
for input, inputSpec := range template.File.Input {
|
||||
template.Input[input] = inputSpec.Default
|
||||
}
|
||||
|
||||
for key, val := range fixture.vars {
|
||||
template.Input[key] = val
|
||||
}
|
||||
|
||||
templatePath, err := templates.TemplatePath(template)
|
||||
require.NoError(t, err)
|
||||
if fixture.path != "" {
|
||||
templatePath = fixture.path
|
||||
}
|
||||
|
||||
actualPath := path.Join("testdata", testName, "actual")
|
||||
expectedPath := path.Join("testdata", testName, "expected")
|
||||
|
||||
templatedFiles, err := loader.TemplateFiles(template, files, path.Join(actualPath, templatePath))
|
||||
require.NoError(t, err, "failed to template files")
|
||||
|
||||
err = os.RemoveAll(actualPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = writer.Write(ctx, ui, templatedFiles)
|
||||
require.NoError(t, err, "failed to write files")
|
||||
|
||||
actualFiles, err := getFiles(actualPath)
|
||||
require.NoError(t, err, "failed to get actual files")
|
||||
|
||||
expectedFiles, err := getFiles(expectedPath)
|
||||
assert.NoError(t, err, "failed to get expected files")
|
||||
|
||||
slices.Sort(actualFiles)
|
||||
slices.Sort(expectedFiles)
|
||||
|
||||
assert.Equal(
|
||||
t,
|
||||
makeRelative(expectedPath, expectedFiles),
|
||||
makeRelative(actualPath, actualFiles),
|
||||
"expected and actual files didn't match",
|
||||
)
|
||||
|
||||
compareFiles(t,
|
||||
expectedPath, actualPath,
|
||||
expectedFiles, actualFiles,
|
||||
)
|
||||
})
|
||||
|
||||
return f
|
||||
}
|
||||
|
||||
func compareFiles(t *testing.T, expectedPath, actualPath string, expectedFiles, actualFiles []string) {
|
||||
expectedRelativeFiles := makeRelative(expectedPath, expectedFiles)
|
||||
actualRelativeFiles := makeRelative(actualPath, actualFiles)
|
||||
|
||||
for expectedIndex, expectedRelativeFile := range expectedRelativeFiles {
|
||||
for actualIndex, actualRelativeFile := range actualRelativeFiles {
|
||||
if expectedRelativeFile == actualRelativeFile {
|
||||
expectedFilePath := expectedFiles[expectedIndex]
|
||||
actualFilePath := actualFiles[actualIndex]
|
||||
|
||||
expectedFile, err := os.ReadFile(expectedFilePath)
|
||||
require.NoError(t, err, "failed to read expected file")
|
||||
|
||||
actualFile, err := os.ReadFile(actualFilePath)
|
||||
require.NoError(t, err, "failed to read actual file")
|
||||
|
||||
assert.Equal(t,
|
||||
string(expectedFile), string(actualFile),
|
||||
"expected and actual file doesn't match\n\texpected path=%s\n\t actual path=%s",
|
||||
expectedFilePath, actualFilePath,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeRelative, test files are prefixed with either actual or expected, this makes it hard to compare, this makes them comparable by removing their unique folder prefix.
|
||||
func makeRelative(prefix string, filePaths []string) []string {
|
||||
output := make([]string, 0, len(filePaths))
|
||||
for _, filePath := range filePaths {
|
||||
relative := strings.TrimPrefix(strings.TrimPrefix(filePath, prefix), "/")
|
||||
|
||||
output = append(output, relative)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func find(templates []templates.Template, templateName string) (*templates.Template, error) {
|
||||
templateNames := make([]string, 0)
|
||||
for _, template := range templates {
|
||||
if template.File.Name == templateName {
|
||||
return &template, nil
|
||||
}
|
||||
|
||||
templateNames = append(templateNames, template.File.Name)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("template was not found: %s", strings.Join(templateNames, ", "))
|
||||
}
|
||||
|
||||
func getFiles(root string) ([]string, error) {
|
||||
actualFiles := make([]string, 0)
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Type().IsRegular() {
|
||||
actualFiles = append(actualFiles, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return actualFiles, nil
|
||||
}
|
15
main.go
Normal file
15
main.go
Normal file
@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.Execute(); err != nil {
|
||||
fmt.Printf("scaffold failed: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package {{ "{{ .Input.package }}" }}
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
8
registry/scaffold/files/scaffold.yaml.gotmpl
Normal file
8
registry/scaffold/files/scaffold.yaml.gotmpl
Normal file
@ -0,0 +1,8 @@
|
||||
name: {{ ReplaceAll .Input.name "-" "_" }}
|
||||
default:
|
||||
path: internal/app/
|
||||
input:
|
||||
package:
|
||||
type: string
|
||||
description: "Which go pkg to use for the generated file"
|
||||
default: "app"
|
18
registry/scaffold/files/scaffold_test.go.gotmpl
Normal file
18
registry/scaffold/files/scaffold_test.go.gotmpl
Normal file
@ -0,0 +1,18 @@
|
||||
package {{ ReplaceAll .Input.name "-" "" }}
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/tests"
|
||||
)
|
||||
|
||||
func TestScaffold(t *testing.T) {
|
||||
tests.
|
||||
Test(t, "{{ ReplaceAll .Input.name "-" "" }}").
|
||||
ScaffoldDefaultTest("default").
|
||||
ScaffoldTest("scaffold package with name",
|
||||
func(fixture *tests.ScaffoldFixture) {
|
||||
fixture.WithVariable("package", "somename")
|
||||
},
|
||||
)
|
||||
}
|
9
registry/scaffold/files/testdata/default/actual/internal/app/externalhttp.go
vendored
Normal file
9
registry/scaffold/files/testdata/default/actual/internal/app/externalhttp.go
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
package app
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
9
registry/scaffold/files/testdata/default/expected/internal/app/externalhttp.go
vendored
Normal file
9
registry/scaffold/files/testdata/default/expected/internal/app/externalhttp.go
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
package app
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package something
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package somename
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
8
registry/scaffold/scaffold.yaml
Normal file
8
registry/scaffold/scaffold.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
name: scaffold
|
||||
default:
|
||||
path: >
|
||||
registry/{{ ReplaceAll .Input.name "-" "_" }}
|
||||
input:
|
||||
name:
|
||||
type: string
|
||||
description: "which name to use for the scaffold"
|
17
registry/scaffold/scaffold_test.go
Normal file
17
registry/scaffold/scaffold_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package externalhttp_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/tests"
|
||||
)
|
||||
|
||||
func TestScaffold(t *testing.T) {
|
||||
tests.
|
||||
Test(t, "scaffold").
|
||||
ScaffoldTest("scaffold itself",
|
||||
func(fixture *tests.ScaffoldFixture) {
|
||||
fixture.WithVariable("name", "some-name")
|
||||
},
|
||||
)
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package {{ .Input.package }}
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
8
registry/scaffold/testdata/scaffold_itself/expected/registry/some_name/scaffold.yaml
vendored
Normal file
8
registry/scaffold/testdata/scaffold_itself/expected/registry/some_name/scaffold.yaml
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
name: some_name
|
||||
default:
|
||||
path: internal/app/
|
||||
input:
|
||||
package:
|
||||
type: string
|
||||
description: "Which go pkg to use for the generated file"
|
||||
default: "app"
|
18
registry/scaffold/testdata/scaffold_itself/expected/registry/some_name/scaffold_test.go
vendored
Normal file
18
registry/scaffold/testdata/scaffold_itself/expected/registry/some_name/scaffold_test.go
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
package somename
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.front.kjuulh.io/kjuulh/scaffold/internal/tests"
|
||||
)
|
||||
|
||||
func TestScaffold(t *testing.T) {
|
||||
tests.
|
||||
Test(t, "somename").
|
||||
ScaffoldDefaultTest("default").
|
||||
ScaffoldTest("scaffold package with name",
|
||||
func(fixture *tests.ScaffoldFixture) {
|
||||
fixture.WithVariable("package", "somename")
|
||||
},
|
||||
)
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package app
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package app
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package something
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package somename
|
||||
|
||||
import "git.front.kjuulh.io/kjuulh/scaffoldhttp"
|
||||
|
||||
func externalHttpServer() func() (*http.ExternalServer, error) {
|
||||
return func() (*http.ExternalServer, error) {
|
||||
return http.NewExternal(), nil
|
||||
}
|
||||
}
|
7
renovate.json
Normal file
7
renovate.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"local>lunarway/renovate-config",
|
||||
"local>lunarway/renovate-config//presets/golang-libs"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user