diff --git a/.examples/hooks/hooks/post_scaffold b/.examples/hooks/hooks/post_scaffold new file mode 100644 index 0000000..a091260 --- /dev/null +++ b/.examples/hooks/hooks/post_scaffold @@ -0,0 +1,4 @@ +#!/usr/bin/env python3 + +with open("{{ .ProjectKebab }}/file.txt", "a") as f: + f.write("Hello\n") diff --git a/.examples/hooks/scaffold.yaml b/.examples/hooks/scaffold.yaml new file mode 100644 index 0000000..489e9dd --- /dev/null +++ b/.examples/hooks/scaffold.yaml @@ -0,0 +1,3 @@ +presets: + default: + Project: "scaffold-test-default" diff --git a/.examples/hooks/{{ .ProjectKebab }}/file.txt b/.examples/hooks/{{ .ProjectKebab }}/file.txt new file mode 100644 index 0000000..6aa652f --- /dev/null +++ b/.examples/hooks/{{ .ProjectKebab }}/file.txt @@ -0,0 +1 @@ +Hook says: diff --git a/app/commands/cmd_new.go b/app/commands/cmd_new.go index b0edd8d..2a5dd50 100644 --- a/app/commands/cmd_new.go +++ b/app/commands/cmd_new.go @@ -79,10 +79,10 @@ func (ctrl *Controller) New(args []string, flags FlagsNew) error { outfs := ctrl.Flags.OutputFS() err = ctrl.runscaffold(runconf{ - scaffolddir: path, - showMessages: !flags.NoPrompt, - varfunc: varfunc, - outputfs: outfs, + scaffolddir: path, + noPrompt: flags.NoPrompt, + varfunc: varfunc, + outputfs: outfs, }) if err != nil { return err diff --git a/app/commands/runner.go b/app/commands/runner.go index 67922db..0c7b5eb 100644 --- a/app/commands/runner.go +++ b/app/commands/runner.go @@ -1,10 +1,15 @@ package commands import ( + "errors" "fmt" + "io/fs" "os" + "path/filepath" + "strings" "github.com/charmbracelet/glamour" + "github.com/charmbracelet/huh" "github.com/hay-kot/scaffold/app/core/rwfs" "github.com/hay-kot/scaffold/app/scaffold" ) @@ -12,8 +17,8 @@ import ( type runconf struct { // os path to the scaffold directory. scaffolddir string - // showMessages is a flag to show pre/post messages. - showMessages bool + // noPrompt is a flag to show pre/post messages. + noPrompt bool // varfunc is a function that returns a map of variables that is provided // to the template engine. varfunc func(*scaffold.Project) (map[string]any, error) @@ -25,15 +30,15 @@ type runconf struct { // so that we can allow the `test` and `new` commands to share as much of the same code // as possible. func (ctrl *Controller) runscaffold(cfg runconf) error { - pfs := os.DirFS(cfg.scaffolddir) - p, err := scaffold.LoadProject(pfs, scaffold.Options{ + scaffoldFS := os.DirFS(cfg.scaffolddir) + p, err := scaffold.LoadProject(scaffoldFS, scaffold.Options{ NoClobber: ctrl.Flags.NoClobber, }) if err != nil { return err } - if cfg.showMessages && p.Conf.Messages.Pre != "" { + if !cfg.noPrompt && p.Conf.Messages.Pre != "" { out, err := glamour.RenderWithEnvironmentConfig(p.Conf.Messages.Pre) if err != nil { return err @@ -49,7 +54,7 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { args := &scaffold.RWFSArgs{ Project: p, - ReadFS: pfs, + ReadFS: scaffoldFS, WriteFS: cfg.outputfs, } @@ -63,7 +68,14 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { return err } - if cfg.showMessages && p.Conf.Messages.Post != "" { + if ctrl.rc.Settings.RunHooks != scaffold.RunHooksNever { + err = ctrl.runHook(scaffoldFS, cfg.outputfs, scaffold.PostScaffoldScripts, vars, cfg.noPrompt) + if err != nil { + return err + } + } + + if !cfg.noPrompt && p.Conf.Messages.Post != "" { rendered, err := ctrl.engine.TmplString(p.Conf.Messages.Post, vars) if err != nil { return err @@ -79,3 +91,91 @@ func (ctrl *Controller) runscaffold(cfg runconf) error { return nil } + +func (ctrl *Controller) runHook( + rfs rwfs.ReadFS, + wfs rwfs.WriteFS, + hookPrefix string, + vars any, + noPrompt bool, +) error { + sources, err := fs.ReadDir(rfs, scaffold.HooksDir) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("failed to open %q hook file: %w", hookPrefix, err) + } + return nil + } + + // find first glob match + var hookContents []byte + for _, source := range sources { + if source.IsDir() { + continue + } + + if strings.HasPrefix(source.Name(), hookPrefix) { + path := filepath.Join(scaffold.HooksDir, source.Name()) + + hookContents, err = fs.ReadFile(rfs, path) + if err != nil { + return err + } + } + } + + if len(hookContents) == 0 { + return nil + } + + rendered, err := ctrl.engine.TmplString(string(hookContents), vars) + if err != nil { + return err + } + + if !shouldRunHooks(ctrl.rc.Settings.RunHooks, noPrompt, hookPrefix, rendered) { + return nil + } + + err = wfs.RunHook(hookPrefix, []byte(rendered), nil) + if err != nil && !errors.Is(err, rwfs.ErrHooksNotSupported) { + return err + } + + return nil +} + +// shouldRunHooks will resolve the users RunHooks preference and either return the preference +// or prompt the user for their choice when the preference is RunHooksPrompt +func shouldRunHooks(runPreference scaffold.RunHooksOption, noPrompt bool, name string, rendered string) bool { + for { + switch runPreference { + case scaffold.RunHooksAlways: + return true + case scaffold.RunHooksNever: + return false + case scaffold.RunHooksPrompt: + if noPrompt { + return false + } + + err := huh.Run(huh.NewSelect[scaffold.RunHooksOption](). + Title(fmt.Sprintf("scaffold defines a %s hook", name)). + Options( + huh.NewOption("run", scaffold.RunHooksAlways), + huh.NewOption("skip", scaffold.RunHooksNever), + huh.NewOption("review", scaffold.RunHooksPrompt)). + Value(&runPreference)) + if err != nil { + fmt.Fprint(os.Stderr, err) + return false + } + + if runPreference == scaffold.RunHooksPrompt { + fmt.Printf("\n%s\n", rendered) + } + default: + return false + } + } +} diff --git a/app/core/rwfs/mem.go b/app/core/rwfs/mem.go index fa43a14..6c54c20 100644 --- a/app/core/rwfs/mem.go +++ b/app/core/rwfs/mem.go @@ -34,6 +34,10 @@ func (m *MemoryWFS) WriteFile(path string, data []byte, perm fs.FileMode) error return m.FS.WriteFile(path, data, perm) } +func (m *MemoryWFS) RunHook(name string, data []byte, args []string) error { + return ErrHooksNotSupported +} + func NewMemoryWFS() *MemoryWFS { return &MemoryWFS{ FS: memfs.New(), diff --git a/app/core/rwfs/os.go b/app/core/rwfs/os.go index d52d34b..70e6af6 100644 --- a/app/core/rwfs/os.go +++ b/app/core/rwfs/os.go @@ -1,8 +1,11 @@ package rwfs import ( + "context" "io/fs" "os" + "os/exec" + "os/signal" "path/filepath" ) @@ -34,3 +37,52 @@ func (o *OsWFS) MkdirAll(path string, perm fs.FileMode) error { func (o *OsWFS) WriteFile(name string, data []byte, perm fs.FileMode) error { return os.WriteFile(filepath.Join(o.root, name), data, perm) } + +func (o *OsWFS) RunHook(name string, data []byte, args []string) error { + tmp, err := writeHook(name, data) + + defer func() { + if rerr := os.Remove(tmp); rerr != nil && err == nil { + err = rerr + } + }() + + if err != nil { + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + defer stop() + go func() { + // stop receiving signal notifications as soon as possible. + <-ctx.Done() + stop() + }() + + cmd := exec.CommandContext(ctx, tmp, append([]string{tmp}, args...)...) + cmd.Dir = o.root + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + return err +} + +func writeHook(name string, data []byte) (string, error) { + f, err := os.CreateTemp("", name) + if err != nil { + return "", err + } + + tmp := f.Name() + + err = os.Chmod(tmp, 0o700) + if err != nil { + return tmp, err + } + + _, err = f.Write(data) + if cerr := f.Close(); cerr != nil && err == nil { + err = cerr + } + return tmp, err +} diff --git a/app/core/rwfs/rwfs.go b/app/core/rwfs/rwfs.go index c9bbbf3..a12d42c 100644 --- a/app/core/rwfs/rwfs.go +++ b/app/core/rwfs/rwfs.go @@ -3,9 +3,12 @@ package rwfs import ( + "errors" "io/fs" ) +var ErrHooksNotSupported = errors.New("hooks not supported") + // ReadFS is a read only file system that can be used to read files from // a file system. It is a alias for fs.FS. type ReadFS = fs.FS @@ -16,4 +19,5 @@ type WriteFS interface { fs.FS MkdirAll(path string, perm fs.FileMode) error WriteFile(name string, data []byte, perm fs.FileMode) error + RunHook(name string, data []byte, args []string) error } diff --git a/app/scaffold/project.go b/app/scaffold/project.go index a18cf9a..c2ac1c0 100644 --- a/app/scaffold/project.go +++ b/app/scaffold/project.go @@ -14,6 +14,11 @@ import ( "github.com/rs/zerolog/log" ) +const ( + HooksDir = "hooks" + PostScaffoldScripts = "post_scaffold" +) + var projectNames = [...]string{ "{{ .Project }}", "{{ .ProjectSlug }}", diff --git a/app/scaffold/rc.go b/app/scaffold/rc.go index 1079a78..2b1783d 100644 --- a/app/scaffold/rc.go +++ b/app/scaffold/rc.go @@ -49,8 +49,53 @@ type ScaffoldRC struct { Auth []AuthEntry `yaml:"auth"` } +type RunHooksOption string + +var ( + RunHooksNever RunHooksOption = "never" + RunHooksAlways RunHooksOption = "always" + RunHooksPrompt RunHooksOption = "prompt" +) + +func ParseRunHooksOption(s string) RunHooksOption { + zero := RunHooksOption("") + ptr := &zero + _ = ptr.UnmarshalText([]byte(s)) + return *ptr +} + +func (r *RunHooksOption) UnmarshalText(text []byte) error { + switch string(text) { + case "never", "no", "false": + *r = RunHooksNever + case "always", "yes", "true": + *r = RunHooksAlways + case "prompt", "": // if left empty, default to prompt + *r = RunHooksPrompt + default: + // fallback to whatever they input so we can log the incorrect value + *r = RunHooksOption(string(text)) + } + + return nil +} + +func (r RunHooksOption) IsValid() bool { + switch r { + case RunHooksNever, RunHooksAlways, RunHooksPrompt: + return true + default: + return false + } +} + +func (r RunHooksOption) String() string { + return string(r) +} + type Settings struct { - Theme styles.HuhTheme `yaml:"theme"` + Theme styles.HuhTheme `yaml:"theme"` + RunHooks RunHooksOption `yaml:"run_hooks"` } type AuthEntry struct { @@ -79,7 +124,8 @@ func (e RcValidationErrors) Error() string { func DefaultScaffoldRC() *ScaffoldRC { return &ScaffoldRC{ Settings: Settings{ - Theme: styles.HuhThemeScaffold, + Theme: styles.HuhThemeScaffold, + RunHooks: RunHooksPrompt, }, } } @@ -133,6 +179,13 @@ func (rc *ScaffoldRC) Validate() error { }) } + if !rc.Settings.RunHooks.IsValid() { + errs = append(errs, RCValidationError{ + Key: "settings.run_hooks", + Cause: fmt.Errorf("invalid run_hooks: %s", rc.Settings.RunHooks.String()), + }) + } + if len(errs) > 0 { return RcValidationErrors(errs) } diff --git a/app/scaffold/rc_test.go b/app/scaffold/rc_test.go index a9c862e..c3dc23a 100644 --- a/app/scaffold/rc_test.go +++ b/app/scaffold/rc_test.go @@ -3,7 +3,9 @@ package scaffold import ( "bytes" "errors" + "fmt" "io" + "strings" "testing" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" @@ -147,3 +149,59 @@ auth: require.False(t, ok) assert.Nil(t, auth) } + +func Test_RunHooksOption_UnmarshalText(t *testing.T) { + type tcase struct { + namefmt string + inputs []string + want RunHooksOption + wantValid bool + } + + tests := []tcase{ + { + namefmt: "valid for 'never' (%s)", + inputs: []string{"never", "no", "false"}, + want: RunHooksNever, + wantValid: true, + }, + { + namefmt: "valid for 'always' (%s)", + inputs: []string{"always", "yes", "true"}, + want: RunHooksAlways, + wantValid: true, + }, + { + namefmt: "valid for 'prompt' (%s)", + inputs: []string{"prompt", ""}, + want: RunHooksPrompt, + wantValid: true, + }, + { + namefmt: "invalid for any option (%s)", + inputs: []string{"invalid", " "}, + wantValid: false, + }, + } + + for _, tt := range tests { + name := fmt.Sprintf(tt.namefmt, strings.Join(tt.inputs, ", ")) + t.Run(name, func(t *testing.T) { + for _, input := range tt.inputs { + var got RunHooksOption + err := got.UnmarshalText([]byte(input)) + + require.NoError(t, err) // UnmarshalText should _never_ error + + switch { + case tt.wantValid: + assert.Equal(t, tt.want, got) + assert.True(t, got.IsValid()) + default: + assert.NotEqual(t, tt.want, got) + assert.False(t, got.IsValid()) + } + } + }) + } +} diff --git a/docs/docs/.vitepress/config.mts b/docs/docs/.vitepress/config.mts index 75f224f..670581c 100644 --- a/docs/docs/.vitepress/config.mts +++ b/docs/docs/.vitepress/config.mts @@ -51,15 +51,10 @@ export default withMermaid( text: "Creating Scaffolds", items: [ { text: "Scaffold File", link: "/templates/scaffold-file" }, + { text: "File Reference", link: "/templates/config-reference" }, { text: "Template Engine", link: "/templates/template-engine" }, - { - text: "File Reference", - link: "/templates/config-reference", - }, - { - text: "Testing Scaffolds", - link: "/templates/testing-scaffolds", - }, + { text: "Hooks", link: "/templates/hooks" }, + { text: "Testing Scaffolds", link: "/templates/testing-scaffolds" }, ], }, ], diff --git a/docs/docs/templates/hooks.md b/docs/docs/templates/hooks.md new file mode 100644 index 0000000..7852ca7 --- /dev/null +++ b/docs/docs/templates/hooks.md @@ -0,0 +1,21 @@ +--- +--- + +# Hooks + +Hooks are files that are stored in the `hooks` subdirectory of your scaffold. They allow you to run scripts at specific points during project generation. They are skipped when the scaffold output directory is an in-memory filesystem or when they are explicitely disabled. The [shebang]() is mandatory and can be set to any interpreter on your system. + +**Note That** + +- Template variables are available in the scripts. +- Currently, only the `post_scaffold` hook is implemented. +- Hooks are matched by checking for a string prefix, so `post_scaffold.sh` will execute on the `post_scaffold` hook. +- Only one hook file is allowed per hook. If multiple files are found, only the first is executed. + +::: tip Working directory +The scripts' working directory is set to the scaffold output directory. +::: + +## `post_scaffold` + +The `post_scaffold` hook is executed after the files have been rendered on the disk, but before the `post` message is printed. It is typically used to fix the formatting of generated files. diff --git a/docs/docs/user-guide/scaffold-rc.md b/docs/docs/user-guide/scaffold-rc.md index 5ce6e1f..696fbc6 100644 --- a/docs/docs/user-guide/scaffold-rc.md +++ b/docs/docs/user-guide/scaffold-rc.md @@ -31,6 +31,21 @@ The Theme settings allows the user to set the default theme for the scaffolding - `base16` - `catppuccino` +### `run_hooks` + +You may disable hooks globally by setting `run_hooks` to `never`, or choose to be prompted before they run by setting it to `prompt`. The `--run-hooks` CLI setting takes precedence. Options include: + +- `always` - run hooks without prompting +- `never` - never run hooks without prompting +- `prompt` - prompt before running hooks (default) + +**Example** + +```yaml +settings: + run_hooks: prompt +``` + ## `defaults` The `defaults` section allows you to set some default values for the scaffolding process. These can be any key/value string pairs diff --git a/go.mod b/go.mod index 3ea2d72..6f4d1f0 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 478e9ad..5920642 100644 --- a/go.sum +++ b/go.sum @@ -30,12 +30,8 @@ github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1l github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6 h1:6nVCV8pqGaeyxetur3gpX3AAaiyKgzjIoCPV3NXKZBE= -github.com/charmbracelet/bubbles v0.17.2-0.20240108170749-ec883029c8e6/go.mod h1:9HxZWlkCqz2PRwsCbYl7a3KXvGzFaDHpYbSYMJ+nE3o= github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= -github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= -github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/bubbletea v0.26.2 h1:Eeb+n75Om9gQ+I6YpbCXQRKHt5Pn4vMwusQpwLiEgJQ= github.com/charmbracelet/bubbletea v0.26.2/go.mod h1:6I0nZ3YHUrQj7YHIHlM8RySX4ZIthTliMY+W8X8b+Gs= github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= @@ -47,8 +43,6 @@ github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy12 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -77,8 +71,6 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMj github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= -github.com/go-sprout/sprout v0.3.0 h1:8reB5lMgCwNVQB/vDkQPs5C0vp3Jh8/4xP7kGJF6bUg= -github.com/go-sprout/sprout v0.3.0/go.mod h1:BG7Zrds7XG7VZvLAkiT3pOK9rLQ6HSJIB4lAXzLdtaA= github.com/go-sprout/sprout v0.4.0 h1:YmXYSxQvlSgShOqSwFRAklph7pW0B05X2A/JL9NCxhs= github.com/go-sprout/sprout v0.4.0/go.mod h1:HcSyVucdA2jn2UOB3793MmWDtf+nSIAs4HV+CZ7YrT4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -156,8 +148,6 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -213,8 +203,6 @@ golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -228,14 +216,11 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -243,8 +228,6 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/main.go b/main.go index fe354fc..676215b 100644 --- a/main.go +++ b/main.go @@ -96,7 +96,12 @@ func main() { Name: "theme", Usage: "theme to use for the scaffold output", Value: "scaffold", - EnvVars: []string{"SCAFFOLD_THEME"}, + EnvVars: []string{"SCAFFOLD_THEME", "SCAFFOLD_SETTINGS_THEME"}, + }, + &cli.StringFlag{ + Name: "run-hooks", + Usage: "run hooks (never, always, prompt) when provided overrides scaffold rc", + EnvVars: []string{"SCAFFOLD_SETTINGS_RUN_HOOKS"}, }, }, Before: func(ctx *cli.Context) error { @@ -156,6 +161,10 @@ func main() { rc.Settings.Theme = styles.HuhTheme(ctx.String("theme")) } + if ctx.IsSet("run-hooks") { + rc.Settings.RunHooks = scaffold.ParseRunHooksOption(ctx.String("run-hooks")) + } + // // Validate Runtime Config // diff --git a/tests/hooks-snapshot.test.sh b/tests/hooks-snapshot.test.sh new file mode 100755 index 0000000..8618b4d --- /dev/null +++ b/tests/hooks-snapshot.test.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Source the assert_snapshot function +source tests/assert.sh + +# Your script continues as before... +output=$($1 --log-level="error" \ + --run-hooks="always" \ + new \ + --preset="default" \ + --no-prompt \ + --snapshot="stdout" \ + hooks) + +# Call the function to assert the snapshot +assert_snapshot "hooks.snapshot.txt" "$output" diff --git a/tests/snapshots/hooks.snapshot.txt b/tests/snapshots/hooks.snapshot.txt new file mode 100644 index 0000000..a11c0f6 --- /dev/null +++ b/tests/snapshots/hooks.snapshot.txt @@ -0,0 +1,5 @@ +scaffold-test-default: (type=dir) + file.txt: (type=file) + Hook says: + Hello +