diff --git a/cmd/commands/check.go b/cmd/commands/check.go index a4fa5697..b09831bf 100644 --- a/cmd/commands/check.go +++ b/cmd/commands/check.go @@ -1,50 +1,30 @@ package commands import ( - "path/filepath" - "strings" - "github.com/caffeine-addictt/waku/internal/errors" "github.com/caffeine-addictt/waku/internal/log" "github.com/caffeine-addictt/waku/internal/template" - "github.com/caffeine-addictt/waku/internal/utils" "github.com/spf13/cobra" ) var CheckCmd = &cobra.Command{ Use: "check ", Aliases: []string{"ch", "c", "verify"}, - Short: "check if template.json is valid", - Long: "Check if your current template.json is valid", + Short: "check if config is valid", + Long: "Check if your current config is valid", Args: cobra.MaximumNArgs(1), SilenceErrors: true, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Check for naming - if len(args) == 1 && !strings.HasSuffix(args[0], "template.json") { - return errors.NewWakuErrorf("name your file template.json") - } - - // Resolve file path var filePath string if len(args) == 1 { filePath = args[0] } else { - filePath = "template.json" - } - filePath = filepath.Clean(filePath) - - log.Debugf("checking if %s is a file\n", filePath) - ok, err := utils.IsFile(filePath) - if err != nil { - return errors.NewWakuErrorf("failed to check if %s is a file: %v", filePath, err) - } - if !ok { - return errors.NewWakuErrorf("%s does not exist or is not a file", filePath) + filePath = "." } log.Debugf("checking if %s is a valid template\n", filePath) - if _, err := template.ParseConfig(filePath); err != nil { + if _, _, err := template.ParseConfig(filePath); err != nil { return errors.ToWakuError(err) } diff --git a/cmd/commands/new.go b/cmd/commands/new.go index 72173fe6..694b7ee5 100644 --- a/cmd/commands/new.go +++ b/cmd/commands/new.go @@ -38,15 +38,23 @@ var NewCmd = &cobra.Command{ var projectRootDir string var license license.License + log.Debugln("Creating name and license prompts...") + namePrompt := template.PromptForProjectName(&name, &projectRootDir) licenseSelect, err := template.PromptForLicense(&license) if err != nil { return errors.ToWakuError(err) } - if err := huh.NewForm( - huh.NewGroup(template.PromptForProjectName(&name, &projectRootDir)), - huh.NewGroup(licenseSelect), - ).WithAccessible(options.GlobalOpts.Accessible).Run(); err != nil { + initialPrompts := make([]*huh.Group, 0, 2) + if namePrompt != nil { + initialPrompts = append(initialPrompts, huh.NewGroup(namePrompt)) + } + if licenseSelect != nil { + initialPrompts = append(initialPrompts, huh.NewGroup(licenseSelect)) + } + + log.Debugln("running prompts...") + if err := huh.NewForm(initialPrompts...).WithAccessible(options.GlobalOpts.Accessible).Run(); err != nil { return errors.ToWakuError(err) } @@ -91,8 +99,8 @@ var NewCmd = &cobra.Command{ } // Parse template.json - log.Infoln("Parsing template.json...") - tmpl, err := template.ParseConfig(filepath.Join(rootDir, "template.json")) + log.Infoln("Parsing config...") + configFilePath, tmpl, err := template.ParseConfig(rootDir) if err != nil { return errors.ToWakuError(err) } @@ -182,7 +190,7 @@ var NewCmd = &cobra.Command{ ignoreRules := types.NewSet( ".git/", "LICENSE*", - "template.json", + configFilePath, ) if tmpl.Ignore != nil { ignoreRules.Union(types.Set[string](*tmpl.Ignore)) @@ -241,7 +249,7 @@ func init() { func AddNewCmdFlags(cmd *cobra.Command) { cmd.Flags().VarP(&options.NewOpts.Repo, "repo", "r", "source repository to template from") cmd.Flags().VarP(&options.NewOpts.Branch, "branch", "b", "branch to clone from") - cmd.Flags().VarP(&options.NewOpts.Directory, "directory", "D", "directory where 'template.json' is located") + cmd.Flags().VarP(&options.NewOpts.Directory, "directory", "D", "directory where config is located") cmd.Flags().VarP(&options.NewOpts.Name, "name", "n", "name of the project") cmd.Flags().VarP(&options.NewOpts.License, "license", "l", "license to use for the project") cmd.Flags().VarP(&options.NewOpts.Style, "style", "S", "which style to use") diff --git a/cmd/options/new.go b/cmd/options/new.go index 4d79e1e6..73edeb5a 100644 --- a/cmd/options/new.go +++ b/cmd/options/new.go @@ -72,7 +72,9 @@ func (o *NewOptions) Validate() error { return err } if o.Branch.Value() == "" { - if err := o.Branch.Set("v" + version.Version); err != nil { + newBranch := "v" + version.Version + log.Debugf("Setting branch to %s\n", newBranch) + if err := o.Branch.Set(newBranch); err != nil { return err } } diff --git a/go.mod b/go.mod index 739c390e..797cc34a 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/muesli/roff v0.1.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -60,5 +61,4 @@ require ( golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/license/store.go b/internal/license/store.go index 54ac14c8..3078103f 100644 --- a/internal/license/store.go +++ b/internal/license/store.go @@ -1,16 +1,18 @@ package license import ( + "fmt" "io" "net/http" - "github.com/caffeine-addictt/waku/pkg/version" + "github.com/caffeine-addictt/waku/cmd/options" + "github.com/caffeine-addictt/waku/internal/log" "github.com/goccy/go-json" ) const ( LICENSE_LIST = "license.json" - BASE_URL = "https://raw.githubusercontent.com/caffeine-addictt/waku/v" + version.Version + "/licenses/" + BASE_URL = "https://raw.githubusercontent.com/caffeine-addictt/waku/%s/licenses/" ) // The global "cache" per say so we only @@ -24,7 +26,9 @@ func GetLicenses() (*[]License, error) { return Licenses, nil } - req, err := http.NewRequest(http.MethodGet, BASE_URL+LICENSE_LIST, http.NoBody) + url := fmt.Sprintf(BASE_URL, options.NewOpts.Branch.Value()) + LICENSE_LIST + log.Infof("Fetching licenses from %s...\n", url) + req, err := http.NewRequest(http.MethodGet, url, http.NoBody) if err != nil { return nil, err } @@ -37,6 +41,7 @@ func GetLicenses() (*[]License, error) { } defer res.Body.Close() + log.Debugln("Reading http stream") body, err := io.ReadAll(res.Body) if err != nil { return nil, err @@ -45,6 +50,8 @@ func GetLicenses() (*[]License, error) { var l struct { Licenses []License `json:"licenses"` } + + log.Debugln("Unmarshalling license json") if err := json.Unmarshal(body, &l); err != nil { return nil, err } diff --git a/internal/license/types.go b/internal/license/types.go index 70cc3136..bd92ddd4 100644 --- a/internal/license/types.go +++ b/internal/license/types.go @@ -23,7 +23,7 @@ type License struct { // The filename in 'licenses/' Filename string `json:"filename"` - // That values the licens wants + // That values the license wants Wants LicenseWants `json:"wants"` } diff --git a/internal/template/ignore.go b/internal/template/ignore.go index 02fffb00..23638bb6 100644 --- a/internal/template/ignore.go +++ b/internal/template/ignore.go @@ -60,14 +60,14 @@ func handleMatching(paths *types.Set[string], pattern string) []string { // convert pattern parts to regex-able nonRecursePartsCount := 0 - isRecusive := false + isRecursive := false newPattern := "^" a: for nonRecursePartsCount < len(patternParts) { switch patternParts[nonRecursePartsCount] { case "**", "": - isRecusive = true + isRecursive = true break a default: s := patternParts[nonRecursePartsCount] @@ -97,7 +97,7 @@ a: continue } - if !isRecusive || (isRecusive && len(pParts) > nonRecursePartsCount) { + if !isRecursive || (isRecursive && len(pParts) > nonRecursePartsCount) { matching = append(matching, p) } } diff --git a/internal/template/parse_config.go b/internal/template/parse_config.go index 09843bfb..ff7dba80 100644 --- a/internal/template/parse_config.go +++ b/internal/template/parse_config.go @@ -1,45 +1,48 @@ package template import ( - "bufio" "encoding/json" - "os" + "io" "path/filepath" + "strings" "github.com/caffeine-addictt/waku/internal/log" "github.com/caffeine-addictt/waku/pkg/config" + "gopkg.in/yaml.v3" ) -func ParseConfig(filePath string) (*config.TemplateJson, error) { - file, err := os.Open(filepath.Clean(filePath)) +func ParseConfig(filePath string) (string, *config.TemplateJson, error) { + path, file, err := GetWakuConfig(filePath) if err != nil { - return nil, err + return "", nil, err } + defer file.Close() - var template config.TemplateJson - var jsonData string - - // Read the entire file content - scanner := bufio.NewScanner(file) - for scanner.Scan() { - jsonData += scanner.Text() - } - - if err := scanner.Err(); err != nil { - return nil, err + log.Debugf("reading config file at %v\n", filePath) + data, err := io.ReadAll(file) + if err != nil { + return "", nil, err } // Unmarshal JSON data - log.Debugln("Unmarshalling JSON data from " + filePath) - if err := json.Unmarshal([]byte(jsonData), &template); err != nil { - return nil, err + var template config.TemplateJson + if strings.HasSuffix(path, "json") { + log.Debugln("unmarshalling JSON data from " + path) + if err := json.Unmarshal(data, &template); err != nil { + return "", nil, err + } + } else { + log.Debugln("unmarshalling YAML data from " + path) + if err := yaml.Unmarshal(data, &template); err != nil { + return "", nil, err + } } - log.Debugf("Unmarshalled JSON data: %+v\n", template) - log.Infoln("Validating JSON data from " + filePath) - if err := template.Validate(filepath.Dir(filePath)); err != nil { - return nil, err + log.Debugf("unmarshalled data: %+v\n", template) + log.Infoln("validating data from " + path) + if err := template.Validate(filepath.Dir(path)); err != nil { + return "", nil, err } - return &template, nil + return path, &template, nil } diff --git a/internal/template/prompting.go b/internal/template/prompting.go index bc926c72..5646d035 100644 --- a/internal/template/prompting.go +++ b/internal/template/prompting.go @@ -7,6 +7,7 @@ import ( "github.com/caffeine-addictt/waku/cmd/options" "github.com/caffeine-addictt/waku/internal/license" + "github.com/caffeine-addictt/waku/internal/log" "github.com/caffeine-addictt/waku/internal/searching" "github.com/caffeine-addictt/waku/internal/sorting" "github.com/caffeine-addictt/waku/internal/types" @@ -100,6 +101,7 @@ func validateLicense(ll *[]license.License, optsL []string, val string, setV *li func PromptForProjectName(name, projectRootDir *string) *huh.Input { if options.NewOpts.Name.Value() != "" { if err := validateProjectName(options.NewOpts.Name.Value(), name, projectRootDir); err == nil { + log.Debugf("name prefilled and is valid: %s\n", options.NewOpts.Name.Value()) return nil } } diff --git a/internal/template/template.go b/internal/template/template.go new file mode 100644 index 00000000..e671b86d --- /dev/null +++ b/internal/template/template.go @@ -0,0 +1,139 @@ +// Waku's template related functions +package template + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/caffeine-addictt/waku/internal/log" +) + +// Filenames is a list of all possible template filenames +// excluding extension +var Filenames [12]string = [12]string{ + "waku.yml", + "template.yml", + "waku.yaml", + "template.yaml", + "waku.json", + "template.json", + ".waku.yml", + ".template.yml", + ".waku.yaml", + ".template.yaml", + ".waku.json", + ".template.json", +} + +func GetWakuConfig(p string) (string, *os.File, error) { + // if p is a file, treat it as a config file + if p != "." { + f, err := openConfigFile(p) + if err != nil { + return "", nil, err + } + if f != nil { + return p, f, nil + } + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + var once sync.Once + var wg sync.WaitGroup + wg.Add(len(Filenames)) + + errChan := make(chan error, 1) + pathChan := make(chan string, 1) + fileChan := make(chan *os.File, 1) + + for _, name := range Filenames { + go func(p, name string) { + defer wg.Done() + path := filepath.Join(p, name) + + select { + case <-ctx.Done(): + log.Debugf("skipping reading config file %s: %s\n", path, ctx.Err()) + return + default: + } + + f, err := openConfigFile(path) + if err != nil { + once.Do(func() { + errChan <- fmt.Errorf("failed to open config file %s: %s", path, err) + cancel() + }) + return + } + if f == nil { + return + } + + select { + case <-ctx.Done(): + defer f.Close() + log.Debugf("closing config file read at %s as context is done\n", path) + default: + once.Do(func() { + log.Debugf("successfully opened config file %s\n", path) + pathChan <- path + fileChan <- f + cancel() + }) + } + }(p, name) + } + + // cleanup + go func() { + wg.Wait() + log.Debugln("closed config file channels") + close(errChan) + close(pathChan) + close(fileChan) + }() + + select { + case err := <-errChan: + // might also trigger due to all the chan closing + // but no file was found + if err == nil { + return "", nil, fmt.Errorf("no config file found, check that you have a file named: %v", Filenames) + } + + log.Debugf("failed to open config file: %s\n", err) + return "", nil, err + + case path := <-pathChan: + log.Debugf("resolved config file at %s\n", path) + return path, <-fileChan, nil + + case <-ctx.Done(): + log.Debugln("config file read timed out") + return "", nil, ctx.Err() + } +} + +// *os.File can be nil for recoverable errors +func openConfigFile(path string) (*os.File, error) { + path = filepath.Clean(path) + + fi, err := os.Stat(path) + if (fi != nil && fi.IsDir()) || (err != nil && errors.Is(err, os.ErrNotExist)) { + log.Debugf("config file %s does not exist or is a directory\n", path) + return nil, nil + } else if err != nil { + return nil, err + } + + log.Debugf("trying to open config file %s\n", path) + return os.Open(path) +} diff --git a/internal/types/clean_string.go b/internal/types/clean_string.go index 6e24821b..0dc50182 100644 --- a/internal/types/clean_string.go +++ b/internal/types/clean_string.go @@ -6,6 +6,7 @@ import ( "github.com/caffeine-addictt/waku/internal/utils" "github.com/goccy/go-json" + "gopkg.in/yaml.v3" ) // String that has a Clean method that also is invoked on UnmarshalJSON @@ -20,6 +21,21 @@ func (s *CleanString) String() string { return string(*s) } +func (s *CleanString) UnmarshalYAML(node *yaml.Node) error { + var tmp string + if err := node.Decode(&tmp); err != nil { + return err + } + + tmp = utils.CleanString(strings.TrimSpace(tmp)) + if tmp == "" { + return fmt.Errorf("invalid string: %s", string(tmp)) + } + + *s = CleanString(tmp) + return nil +} + func (s *CleanString) UnmarshalJSON(data []byte) error { var tmp string if err := json.Unmarshal(data, &tmp); err != nil { diff --git a/internal/types/clean_string_test.go b/internal/types/clean_string_test.go index 675901e8..ccabb564 100644 --- a/internal/types/clean_string_test.go +++ b/internal/types/clean_string_test.go @@ -5,6 +5,7 @@ import ( "github.com/caffeine-addictt/waku/internal/types" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestCleanStringUnmarshalJSON(t *testing.T) { @@ -30,3 +31,27 @@ func TestCleanStringUnmarshalJSON(t *testing.T) { } } } + +func TestCleanStringUnmarshalYAML(t *testing.T) { + tt := []struct { + in string + errors bool + }{ + {"aa", false}, + {"\r\b\n\t", true}, + {"", true}, + {" ", true}, + {"\r\ns", false}, + } + + for _, tc := range tt { + var s types.CleanString + err := yaml.Unmarshal([]byte("\""+tc.in+"\""), &s) + + if tc.errors { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + } +} diff --git a/internal/types/color.go b/internal/types/color.go index 0d2d0f9f..5e0008b9 100644 --- a/internal/types/color.go +++ b/internal/types/color.go @@ -5,6 +5,7 @@ import ( "regexp" "github.com/goccy/go-json" + "gopkg.in/yaml.v3" ) type HexColor string @@ -25,3 +26,17 @@ func (c *HexColor) UnmarshalJSON(data []byte) error { *c = HexColor(color) return nil } + +func (c *HexColor) UnmarshalYAML(node *yaml.Node) error { + var color string + if err := node.Decode(&color); err != nil { + return err + } + + if !hexColorRegex.MatchString(string(color)) { + return fmt.Errorf("invalid hex color: %s", color) + } + + *c = HexColor(color) + return nil +} diff --git a/internal/types/color_test.go b/internal/types/color_test.go index 1288b04a..5cc4e7ce 100644 --- a/internal/types/color_test.go +++ b/internal/types/color_test.go @@ -5,25 +5,26 @@ import ( "github.com/caffeine-addictt/waku/internal/types" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) -func TestHexColors(t *testing.T) { - tt := []struct { - in string - rule string - errors bool - }{ - {"#d", "hex too short", true}, - {"#ddd", "hex valid", false}, - {"#ffffff", "hex valid", false}, - {"#fff", "hex valid", false}, - {"#fffaaas", "hex too long", true}, - {"#ff", "hex too short", true}, - {"#ae24d2", "hex valid", false}, - {"sdwa2fw", "invalid letters", true}, - } +var hexColorTT = []struct { + in string + rule string + errors bool +}{ + {"#d", "hex too short", true}, + {"#ddd", "hex valid", false}, + {"#ffffff", "hex valid", false}, + {"#fff", "hex valid", false}, + {"#fffaaas", "hex too long", true}, + {"#ff", "hex too short", true}, + {"#ae24d2", "hex valid", false}, + {"sdwa2fw", "invalid letters", true}, +} - for _, tc := range tt { +func TestHexColorsJSON(t *testing.T) { + for _, tc := range hexColorTT { t.Run(tc.in, func(t *testing.T) { c := types.HexColor(tc.in) @@ -36,3 +37,18 @@ func TestHexColors(t *testing.T) { }) } } + +func TestHexColorsYAML(t *testing.T) { + for _, tc := range hexColorTT { + t.Run(tc.in, func(t *testing.T) { + c := types.HexColor(tc.in) + + err := yaml.Unmarshal([]byte("\""+tc.in+"\""), &c) + if tc.errors { + assert.Error(t, err, tc.rule) + } else { + assert.NoError(t, err, tc.rule) + } + }) + } +} diff --git a/internal/types/regex.go b/internal/types/regex.go index 1abea604..7937f2e8 100644 --- a/internal/types/regex.go +++ b/internal/types/regex.go @@ -4,6 +4,7 @@ import ( "regexp" "github.com/goccy/go-json" + "gopkg.in/yaml.v3" ) type RegexString struct { @@ -24,3 +25,18 @@ func (r *RegexString) UnmarshalJSON(data []byte) error { r.Regexp = re return nil } + +func (r *RegexString) UnmarshalYAML(node *yaml.Node) error { + var s string + if err := node.Decode(&s); err != nil { + return err + } + + re, err := regexp.Compile(s) + if err != nil { + return err + } + + r.Regexp = re + return nil +} diff --git a/internal/types/regex_test.go b/internal/types/regex_test.go index ddd864fc..54d99ca0 100644 --- a/internal/types/regex_test.go +++ b/internal/types/regex_test.go @@ -6,54 +6,55 @@ import ( "github.com/caffeine-addictt/waku/internal/types" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) -func TestRegexString_UnmarshalJSON(t *testing.T) { - tt := []struct { - input string - expectedRegex string - rule string - expectError bool - }{ - { - input: `"^a.*b$"`, - expectedRegex: "^a.*b$", - rule: "valid regex", - expectError: false, - }, - { - input: `"\\d{3}-\\d{2}-\\d{4}"`, - expectedRegex: "\\d{3}-\\d{2}-\\d{4}", - rule: "Valid regex for SSN", - expectError: false, - }, - { - input: `"invalid(regex"`, - expectedRegex: "", - rule: "invalid regex", - expectError: true, - }, - { - input: `"^$"`, - expectedRegex: "^$", - rule: "valid regex for empty string", - expectError: false, - }, - { - input: `"(?i)abc"`, - expectedRegex: "(?i)abc", - rule: "valid regex with case insensitive flag", - expectError: false, - }, - { - input: `"^@?(.*?)\\s*$"`, - expectedRegex: "^@?(.*?)\\s*$", - rule: "valid regex with optional leading @", - expectError: false, - }, - } +var regexStringTT = []struct { + input string + expectedRegex string + rule string + expectError bool +}{ + { + input: `"^a.*b$"`, + expectedRegex: "^a.*b$", + rule: "valid regex", + expectError: false, + }, + { + input: `"\\d{3}-\\d{2}-\\d{4}"`, + expectedRegex: "\\d{3}-\\d{2}-\\d{4}", + rule: "Valid regex for SSN", + expectError: false, + }, + { + input: `"invalid(regex"`, + expectedRegex: "", + rule: "invalid regex", + expectError: true, + }, + { + input: `"^$"`, + expectedRegex: "^$", + rule: "valid regex for empty string", + expectError: false, + }, + { + input: `"(?i)abc"`, + expectedRegex: "(?i)abc", + rule: "valid regex with case insensitive flag", + expectError: false, + }, + { + input: `"^@?(.*?)\\s*$"`, + expectedRegex: "^@?(.*?)\\s*$", + rule: "valid regex with optional leading @", + expectError: false, + }, +} - for _, tc := range tt { +func TestRegexStringJSON(t *testing.T) { + for _, tc := range regexStringTT { t.Run(tc.rule, func(t *testing.T) { var r types.RegexString err := json.Unmarshal([]byte(tc.input), &r) @@ -68,3 +69,20 @@ func TestRegexString_UnmarshalJSON(t *testing.T) { }) } } + +func TestRegexStringYAML(t *testing.T) { + for _, tc := range regexStringTT { + t.Run(tc.rule, func(t *testing.T) { + var r types.RegexString + err := yaml.Unmarshal([]byte(tc.input), &r) + + if tc.expectError { + assert.Error(t, err) + return + } + assert.NoError(t, err) + + assert.Equal(t, tc.expectedRegex, r.String()) + }) + } +} diff --git a/internal/types/set.go b/internal/types/set.go index 98351893..d09e870d 100644 --- a/internal/types/set.go +++ b/internal/types/set.go @@ -1,6 +1,9 @@ package types -import "github.com/goccy/go-json" +import ( + "github.com/goccy/go-json" + "gopkg.in/yaml.v3" +) // A set implementation using map under the hood. type Set[T comparable] map[T]struct{} @@ -110,3 +113,18 @@ func (s *Set[T]) UnmarshalJSON(data []byte) error { func (s Set[T]) MarshalJSON() ([]byte, error) { return json.Marshal(s.ToSlice()) } + +// UnmarshalYAML unmarshals a YAML string into a set +func (s *Set[T]) UnmarshalYAML(node *yaml.Node) error { + var items []T + if err := node.Decode(&items); err != nil { + return err + } + *s = NewSet(items...) + return nil +} + +// MarshalYAML marshals a set into a YAML string +func (s Set[T]) MarshalYAML() (interface{}, error) { + return s.ToSlice(), nil +} diff --git a/internal/types/set_test.go b/internal/types/set_test.go index 97d9ab91..0d55ff38 100644 --- a/internal/types/set_test.go +++ b/internal/types/set_test.go @@ -7,6 +7,7 @@ import ( "github.com/caffeine-addictt/waku/internal/types" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestSetNewSet(t *testing.T) { @@ -83,3 +84,22 @@ func TestSetUnmarshalJSON(t *testing.T) { assert.True(t, newSet.Contains(2), "expected set to contain 2 after unmarshal") assert.True(t, newSet.Contains(3), "expected set to contain 3 after unmarshal") } + +func TestSetMarshalYAML(t *testing.T) { + set := types.NewSet(1, 2, 3) + data, err := yaml.Marshal(set) + assert.NoError(t, err, "unexpected error during Marshal") + + assert.ElementsMatch(t, []string{"1", "2", "3", ""}, strings.Split(strings.ReplaceAll(string(data), "- ", ""), "\n"), "expected YAML output to match") +} + +func TestSetUnmarshalYAML(t *testing.T) { + var newSet types.Set[int] + data := []byte(`[1,2,3]`) + err := yaml.Unmarshal(data, &newSet) + assert.NoError(t, err, "unexpected error during Unmarshal") + assert.Equal(t, 3, newSet.Len(), "expected length 3 after unmarshal") + assert.True(t, newSet.Contains(1), "expected set to contain 1 after unmarshal") + assert.True(t, newSet.Contains(2), "expected set to contain 2 after unmarshal") + assert.True(t, newSet.Contains(3), "expected set to contain 3 after unmarshal") +} diff --git a/internal/types/value_guard.go b/internal/types/value_guard.go index e36938e2..05d85472 100644 --- a/internal/types/value_guard.go +++ b/internal/types/value_guard.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/goccy/go-json" + "gopkg.in/yaml.v3" ) // A value guard to validate values being set @@ -84,12 +85,22 @@ func (v *ValueGuard[T]) UnmarshalJSON(data []byte) error { return err } - if err := v.Set(tmp); err != nil { - return err - } - return nil + return v.Set(tmp) } func (v ValueGuard[T]) MarshalJSON() ([]byte, error) { return json.Marshal(v.value) } + +func (v *ValueGuard[T]) UnmarshalYAML(node *yaml.Node) error { + var tmp T + if err := node.Decode(&tmp); err != nil { + return err + } + + return v.Set(tmp) +} + +func (v ValueGuard[T]) MarshalYAML() (interface{}, error) { + return v.value, nil +} diff --git a/internal/types/value_guard_test.go b/internal/types/value_guard_test.go index d9d6f119..c44ba9bd 100644 --- a/internal/types/value_guard_test.go +++ b/internal/types/value_guard_test.go @@ -7,6 +7,7 @@ import ( "github.com/caffeine-addictt/waku/internal/types" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestNoParsing(t *testing.T) { @@ -51,6 +52,20 @@ func TestParsingJsonMarshal(t *testing.T) { assert.Equal(t, `"hi"`, string(data), "value should match") } +func TestParsingYamlUnMarshal(t *testing.T) { + var tmp types.ValueGuard[string] + err := yaml.Unmarshal([]byte(`"test"`), &tmp) + assert.NoError(t, err, "failed to unmarshal") + checkValues(t, "test", "", &tmp) +} + +func TestParsingYamlMarshal(t *testing.T) { + tmp := types.NewValueGuardNoParsing("hi", "ok") + data, err := yaml.Marshal(tmp) + assert.NoError(t, err, "failed to marshal") + assert.Equal(t, "hi\n", string(data), "value should match") +} + func TestParsingFailEarly(t *testing.T) { val := "test fail" typeString := "my type" diff --git a/pkg/config/labels.go b/pkg/config/labels.go index 93119270..10499714 100644 --- a/pkg/config/labels.go +++ b/pkg/config/labels.go @@ -3,7 +3,7 @@ package config import "github.com/caffeine-addictt/waku/internal/types" type TemplateLabel []struct { - Name types.CleanString `json:"name"` - Color types.HexColor `json:"color"` - Desc string `json:"description,omitempty"` + Name types.CleanString `json:"name" yaml:"name"` + Color types.HexColor `json:"color" yaml:"color"` + Desc string `json:"description,omitempty" yaml:"description,omitempty"` } diff --git a/pkg/config/prompts.go b/pkg/config/prompts.go index b48ddfee..b91245f3 100644 --- a/pkg/config/prompts.go +++ b/pkg/config/prompts.go @@ -8,6 +8,7 @@ import ( "github.com/caffeine-addictt/waku/internal/types" "github.com/charmbracelet/huh" "github.com/goccy/go-json" + "gopkg.in/yaml.v3" ) // The type of the prompt response @@ -37,13 +38,13 @@ type TemplatePrompts []TemplatePrompt // and Pacal case is recommended. type TemplatePrompt struct { Value any - Format *string `json:"fmt,omitempty"` - Separator *string `json:"sep,omitempty"` - Capture *types.RegexString `json:"capture,omitempty"` - Validate *types.RegexString `json:"validate,omitempty"` - Key types.CleanString `json:"key"` - Ask types.CleanString `json:"ask,omitempty"` - Type TemplatePromptType `json:"type"` + Format *string `json:"fmt,omitempty" yaml:"fmt,omitempty"` + Separator *string `json:"sep,omitempty" yaml:"sep,omitempty"` + Capture *types.RegexString `json:"capture,omitempty" yaml:"capture,omitempty"` + Validate *types.RegexString `json:"validate,omitempty" yaml:"validate,omitempty"` + Key types.CleanString `json:"key" yaml:"key"` + Ask types.CleanString `json:"ask,omitempty" yaml:"ask,omitempty"` + Type TemplatePromptType `json:"type" yaml:"type"` } // FormattedAsk returns the formatted string for the prompt @@ -140,48 +141,72 @@ func (t *TemplatePrompt) formatValue(val string) (string, error) { return l, nil } -func (t *TemplatePrompt) UnmarshalJSON(data []byte) error { - type Alias TemplatePrompt - var s Alias - - if err := json.Unmarshal(data, &s); err != nil { - return err - } - +func unmarshalTemplatePrompt(t *TemplatePrompt) error { // type - s.Type = TemplatePromptType(strings.ToLower(string(s.Type))) - switch s.Type { + t.Type = TemplatePromptType(strings.ToLower(string(t.Type))) + switch t.Type { case TemplatePromptTypeString, TemplatePromptTypeArray: default: - return fmt.Errorf("%s is not a valid prompt type", s.Type) + return fmt.Errorf("%s is not a valid prompt type", t.Type) } // sep - if s.Separator == nil { + if t.Separator == nil { d := string(DefaultTemplatePromptSeparator) - s.Separator = &d + t.Separator = &d } // capture - if s.Capture == nil { - s.Capture = &DefaultTemplatePromptCapture - } else if s.Capture.NumSubexp() != 1 { - return fmt.Errorf("capture %s must have 1 sub-expression", s.Capture.String()) + if t.Capture == nil { + t.Capture = &DefaultTemplatePromptCapture + } else if t.Capture.NumSubexp() != 1 { + return fmt.Errorf("capture %s must have 1 sub-expression", t.Capture.String()) } // format - if s.Format == nil { + if t.Format == nil { d := string(DefaultTemplatePromptFormat) - s.Format = &d - } else if strings.Count(*s.Format, "*")-strings.Count(*s.Format, "\\*") < 1 { - return fmt.Errorf("fmt value '%s' must have at least 1 *", *s.Format) + t.Format = &d + } else if strings.Count(*t.Format, "*")-strings.Count(*t.Format, "\\*") < 1 { + return fmt.Errorf("fmt value '%s' must have at least 1 *", *t.Format) } // validate - if s.Validate == nil { - s.Validate = &DefaultTemplatePromptValidate + if t.Validate == nil { + t.Validate = &DefaultTemplatePromptValidate + } + + return nil +} + +type templatePromptAlias TemplatePrompt + +func (t *TemplatePrompt) UnmarshalYAML(node *yaml.Node) error { + var s templatePromptAlias + if err := node.Decode(&s); err != nil { + return err + } + + ss := TemplatePrompt(s) + if err := unmarshalTemplatePrompt(&ss); err != nil { + return err + } + + return nil +} + +func (t *TemplatePrompt) UnmarshalJSON(data []byte) error { + var s templatePromptAlias + + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + ss := TemplatePrompt(s) + if err := unmarshalTemplatePrompt(&ss); err != nil { + return err } - *t = TemplatePrompt(s) + *t = ss return nil } diff --git a/pkg/config/setup.go b/pkg/config/setup.go index 80d36f0f..3943fd56 100644 --- a/pkg/config/setup.go +++ b/pkg/config/setup.go @@ -19,10 +19,10 @@ const ( // Paths to executable files for post-setup type TemplateSetup struct { - Linux string `json:"linux,omitempty"` - Darwin string `json:"darwin,omitempty"` - Windows string `json:"windows,omitempty"` - Any string `json:"*,omitempty"` + Linux string `json:"linux,omitempty" yaml:"linux,omitempty"` + Darwin string `json:"darwin,omitempty" yaml:"darwin,omitempty"` + Windows string `json:"windows,omitempty" yaml:"windows,omitempty"` + Any string `json:"*,omitempty" yaml:"*,omitempty"` } func (t *TemplateSetup) Validate(root string) error { diff --git a/pkg/config/skip.go b/pkg/config/skip.go deleted file mode 100644 index 622d9e96..00000000 --- a/pkg/config/skip.go +++ /dev/null @@ -1,34 +0,0 @@ -package config - -import ( - "fmt" - "regexp" - - "github.com/goccy/go-json" -) - -type TemplateStep string - -const ( - License TemplateStep = "license" -) - -type TemplateSteps []TemplateStep - -var templateStepsRegexp = regexp.MustCompile(`^license$`) - -func (t *TemplateSteps) UnmarshalJSON(data []byte) error { - var tmp TemplateSteps - if err := json.Unmarshal(data, &tmp); err != nil { - return err - } - - for _, v := range tmp { - if !templateStepsRegexp.MatchString(string(v)) { - return fmt.Errorf("invalid step: %s", tmp) - } - } - - *t = TemplateSteps(tmp) - return nil -} diff --git a/pkg/config/styles.go b/pkg/config/styles.go index 034e8cbc..313bdb12 100644 --- a/pkg/config/styles.go +++ b/pkg/config/styles.go @@ -12,11 +12,11 @@ import ( type TemplateStyles map[types.CleanString]TemplateStyle type TemplateStyle struct { - Setup *TemplateSetup `json:"setup,omitempty"` // Paths to executable files for post-setup - Ignore *TemplateIgnore `json:"ignore,omitempty"` // The files that should be ignored when copying - Labels *TemplateLabel `json:"labels,omitempty"` // The repository labels - Prompts *TemplatePrompts `json:"prompts,omitempty"` // The additional prompts to use - Source types.CleanString `json:"source"` // The source template path + Setup *TemplateSetup `json:"setup,omitempty" yaml:"setup,omitempty"` // Paths to executable files for post-setup + Ignore *TemplateIgnore `json:"ignore,omitempty" yaml:"ignore,omitempty"` // The files that should be ignored when copying + Labels *TemplateLabel `json:"labels,omitempty" yaml:"labels,omitempty"` // The repository labels + Prompts *TemplatePrompts `json:"prompts,omitempty" yaml:"prompts,omitempty"` // The additional prompts to use + Source types.CleanString `json:"source" yaml:"source"` // The source template path } func (t *TemplateStyles) Validate(root string) error { diff --git a/pkg/config/template.go b/pkg/config/template.go index 68abf626..4fc3787a 100644 --- a/pkg/config/template.go +++ b/pkg/config/template.go @@ -6,14 +6,14 @@ import ( "github.com/caffeine-addictt/waku/internal/types" ) -// The template.json file +// The config file type TemplateJson struct { - Setup *TemplateSetup `json:"setup,omitempty"` // Paths to executable files for post-setup - Ignore *TemplateIgnore `json:"ignore,omitempty"` // The files that should be ignored when copying - Labels *TemplateLabel `json:"labels,omitempty"` // The repository labels - Styles *TemplateStyles `json:"styles,omitempty"` // The name of the style mapped to the path to the directory - Prompts *TemplatePrompts `json:"prompts,omitempty"` // The additional prompts to use - Name types.CleanString `json:"name,omitempty"` // The name of the template + Setup *TemplateSetup `json:"setup,omitempty" yaml:"setup,omitempty"` // Paths to executable files for post-setup + Ignore *TemplateIgnore `json:"ignore,omitempty" yaml:"ignore,omitempty"` // The files that should be ignored when copying + Labels *TemplateLabel `json:"labels,omitempty" yaml:"labels,omitempty"` // The repository labels + Styles *TemplateStyles `json:"styles,omitempty" yaml:"styles,omitempty"` // The name of the style mapped to the path to the directory + Prompts *TemplatePrompts `json:"prompts,omitempty" yaml:"prompts,omitempty"` // The additional prompts to use + Name types.CleanString `json:"name,omitempty" yaml:"name,omitempty"` // The name of the template } func (t *TemplateJson) Validate(root string) error { diff --git a/pkg/version/version.go b/pkg/version/version.go index 6c0bd505..5c0788fb 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,4 +1,4 @@ package version // The current app version -const Version = "0.5.1" +const Version = "0.6.0" diff --git a/www/README.md b/www/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/www/docs/blog/posts/2024-10-08-waku-v0.6.0.md b/www/docs/blog/posts/2024-10-08-waku-v0.6.0.md new file mode 100644 index 00000000..7453154c --- /dev/null +++ b/www/docs/blog/posts/2024-10-08-waku-v0.6.0.md @@ -0,0 +1,35 @@ +--- +date: 2024-10-08 +slug: waku-v0.6 +categories: + - announcements +authors: + - alex +--- + +# Announcing Waku v0.6 + +Some very exciting changes have been made to Waku. + +- **new**: Waku now supports [`YAML`](yaml) for configuration. +- **new**: We also now support multiple default configuration filenames. + See the full list [here](../../configuration/introduction.md#filenames). + +As usual, bug fixes, dependency updates, housekeeping, and documentation updates +are included in this release as well. + +## Other news + +Waku has a Discord server. [Join us!](https://discord.gg/NcRFkVTcaw) + +## Download + +You can [install](../../install.md) or upgrade Waku with your favorite package manager, +or see the full release notes [here](https://github.com/caffeine-addictt/waku/releases/tag/v0.3.0). + +## Helping out + +You can help by reporting issues, contributing features, documentation improvements, +and bug fixes. You can also [sponsor the project][sponsor]. + +[sponsor]: https://github.com/sponsors/caffeine-addictt diff --git a/www/docs/configuration/fields/ignore.md b/www/docs/configuration/fields/ignore.md index e17caa21..ada67722 100644 --- a/www/docs/configuration/fields/ignore.md +++ b/www/docs/configuration/fields/ignore.md @@ -3,36 +3,73 @@ Ignore is an array of path globs you want Waku to ignore when templating. -```json -{ - "ignore": [ - // Matching everything. - "*", - - // Matching a specific file. - "path/to.file", - - // Matching any file starting with `f` - // in the `path/` directory. - "path/f*", - - // Matching all files in the `path/` directory - // non-recursively. - "path/*", - - // Matching all files in the `path/` directory - // recursively. - "path/", - "path/**", - - // Placing a `!` infront of any of the above patterns will - // negate the ignore, explicitly including matches. - "!*", - "!path/to.file", - "!path/f*", - "!path/*", - "!path/", - "!path/**" - ] -} -``` +=== "Yaml" + + ```yaml + styles: + my-style: + ignore: + # Matching everything. + - "*" + + # Matching a specific file. + - "path/to.file" + + # Matching any file starting with `f` + # in the `path/` directory. + - "path/f*" + + # Matching all files in the `path/` directory + # non-recursively. + - "path/*" + + # Matching all files in the `path/` directory + # recursively. + - "path/" + - "path/**" + + # Placing a `!` infront of any of the above patterns will + # negate the ignore, explicitly including matches. + - "!*" + - "!path/to.file" + - "!path/f*" + - "!path/*" + - "!path/" + - "!path/**" + ``` + +=== "Json" + + ```json + { + "ignore": [ + // Matching everything. + "*", + + // Matching a specific file. + "path/to.file", + + // Matching any file starting with `f` + // in the `path/` directory. + "path/f*", + + // Matching all files in the `path/` directory + // non-recursively. + "path/*", + + // Matching all files in the `path/` directory + // recursively. + "path/", + "path/**", + + // Placing a `!` infront of any of the above patterns will + // negate the ignore, explicitly including matches. + "!*", + "!path/to.file", + "!path/f*", + "!path/*", + "!path/", + "!path/**" + ] + } + ``` diff --git a/www/docs/configuration/fields/labels.md b/www/docs/configuration/fields/labels.md index 33ff7f8c..0f1f82c8 100644 --- a/www/docs/configuration/fields/labels.md +++ b/www/docs/configuration/fields/labels.md @@ -11,25 +11,47 @@ post-setup for supported [Git](https://git-scm.com/) hosting providers: - **:fontawesome-brands-github: [GitHub](https://github.com)** - **:fontawesome-brands-gitlab: [GitLab](https://gitlab.com)** -## template.json +## waku config -```json -{ - "labels": [ +=== "Yaml" + + ```yaml + styles: + my-style: + labels: + # The name of the label + # (required) + - name: "" + + # The color of the label. + # Only HEX is allowed; #fff, #ffffff + # (required) + color: "" + + # The description of the label + # (optional) + description: "" + ``` + +=== "Json" + + ```json { - // The name of the label - // (required) - "name": "", - - // The color of the label. - // Only HEX is allowed; #fff, #ffffff - // (required) - "color": "", - - // The description of the label - // (optional) - "description": "" + "labels": [ + { + // The name of the label + // (required) + "name": "", + + // The color of the label. + // Only HEX is allowed; #fff, #ffffff + // (required) + "color": "", + + // The description of the label + // (optional) + "description": "" + } + ] } - ] -} -``` + ``` diff --git a/www/docs/configuration/fields/prompts.md b/www/docs/configuration/fields/prompts.md index 7d7d19f6..7ae95d2c 100644 --- a/www/docs/configuration/fields/prompts.md +++ b/www/docs/configuration/fields/prompts.md @@ -7,76 +7,149 @@ Prompts are additionally `asks` for templating. Waku provides some default template values for you. See the docs [here](../templates.md). -## template.json +## waku config -```json -{ - "prompts": [ +=== "Yaml" + + ```yaml + styles: + my-style: + prompts: + # Key is case-sensitive and templated to. + # + # For example, using `key="MyKey"` will + # match `{{{ .MyKey }}}` in files. + # + # (required) + - key: "" + + # Type is the type of value key takes. + # This can be either `str` or `arr`. + # + # If the type is `arr`, string input from the + # user is split based on `sep`, allowing you + # to do iteration with `{{{ range .MyKey }}}{{{ end }}}`. + # + # (required) + type: "" + + # Ask is the prompt that will be shown to the user. + # (optional) + ask: "" + + # Sep is the separator used when type is `arr`. + # + # By default, Waku separates on space. + # + # (optional) + sep: " " + + # Validate is the Regex used to validate the user input. + # + # By default, Waku checks for no empty input. + # + # (optional) + validate: ".+" + + # Capture is the Regex capture group used + # to extract the value from the user input. + # In the case of multiple captures, Waku only + # uses the last capture. + # + # By default, Waku captures everything except + # leading and trailing whitespace. + # + # (optional) + capture: "\s*(.*?)\s*" + + # Format is how each individual value(s) + # are templated. + # + # All instances of `*` not directly after by a + # backslash `\` are replaced with the capture value. + # + # For example, this is how you would ask for temperature + # and ensure that the final templated value has the °C unit: + # capture: "\s*(-?\d+(?:\.\d+)?)\s*°?C?\s*" + # validate: "\s*(-?\d+(?:\.\d+)?)\s*°?C?\s*" + # format: "*°C" + # + # By default, Waku uses the capture value as is. + # + # (optional) + format: "*" + ``` + +=== "Json" + + ```json { - // Key is case-sensitive and templated to. - // - // For example, using `key="MyKey"` will - // match `{{{ .MyKey }}}` in files. - // - // (required) - "key": "", - - // Type is the type of value key takes. - // This can be either `str` or `arr`. - // - // If the type is `arr`, string input from the - // user is split based on `sep`, allowing you - // to do iteration with `{{{ range .MyKey }}}{{{ end }}}`. - // - // (required) - "type": "", - - // Ask is the prompt that will be shown to the user. - // (optional) - "ask": "", - - // Sep is the separator used when type is `arr`. - // - // By default, Waku separates on space. - // - // (optional) - "sep": " ", - - // Validate is the Regex used to validate the user input. - // - // By default, Waku checks for no empty input. - // - // (optional) - "validate": ".+", - - // Capture is the Regex capture group used - // to extract the value from the user input. - // In the case of multiple captures, Waku only - // uses the last capture. - // - // By default, Waku captures everything except - // leading and trailing whitespace. - // - // (optional) - "capture": "\s*(.*?)\s*", - - // Format is how each individual value(s) - // are templated. - // - // All instances of `*` not directly after by a - // backslash `\` are replaced with the capture value. - // - // For example, this is how you would ask for temperature - // and ensure that the final templated value has the °C unit: - // capture: "\s*(-?\d+(?:\.\d+)?)\s*°?C?\s*" - // validate: "\s*(-?\d+(?:\.\d+)?)\s*°?C?\s*" - // format: "*°C" - // - // By default, Waku uses the capture value as is. - // - // (optional) - "format": "*" + "prompts": [ + { + // Key is case-sensitive and templated to. + // + // For example, using `key="MyKey"` will + // match `{{{ .MyKey }}}` in files. + // + // (required) + "key": "", + + // Type is the type of value key takes. + // This can be either `str` or `arr`. + // + // If the type is `arr`, string input from the + // user is split based on `sep`, allowing you + // to do iteration with `{{{ range .MyKey }}}{{{ end }}}`. + // + // (required) + "type": "", + + // Ask is the prompt that will be shown to the user. + // (optional) + "ask": "", + + // Sep is the separator used when type is `arr`. + // + // By default, Waku separates on space. + // + // (optional) + "sep": " ", + + // Validate is the Regex used to validate the user input. + // + // By default, Waku checks for no empty input. + // + // (optional) + "validate": ".+", + + // Capture is the Regex capture group used + // to extract the value from the user input. + // In the case of multiple captures, Waku only + // uses the last capture. + // + // By default, Waku captures everything except + // leading and trailing whitespace. + // + // (optional) + "capture": "\s*(.*?)\s*", + + // Format is how each individual value(s) + // are templated. + // + // All instances of `*` not directly after by a + // backslash `\` are replaced with the capture value. + // + // For example, this is how you would ask for temperature + // and ensure that the final templated value has the °C unit: + // capture: "\s*(-?\d+(?:\.\d+)?)\s*°?C?\s*" + // validate: "\s*(-?\d+(?:\.\d+)?)\s*°?C?\s*" + // format: "*°C" + // + // By default, Waku uses the capture value as is. + // + // (optional) + "format": "*" + } + ] } - ] -} -``` + ``` diff --git a/www/docs/configuration/fields/setup.md b/www/docs/configuration/fields/setup.md index 639a47d8..c61fd673 100644 --- a/www/docs/configuration/fields/setup.md +++ b/www/docs/configuration/fields/setup.md @@ -9,30 +9,58 @@ Setup describes a post-setup script that is optionally ran for each operating system. +## waku config + !!! INFO Only paths relative to the directory - containing `template.json` are allowed. - -```json -{ - "setup": { - // path to a shell script or binary - // (optional) - "linux": "", - - // path to a shell script or binary - // (optional) - "darwin": "", - - // path to a executable or binary - // (optional) - "windows": "", - - // This is the fallback script for unknown - // operating systems. - // (optional) - "*": "" - } -} -``` + containing `waku.yml` are allowed. + +=== "Yaml" + + ```yaml + styles: + my-style: + setup: + # path to a shell script or binary + # (optional) + linux: "" + + # path to a shell script or binary + # (optional) + darwin: "" + + # path to a executable or binary + # (optional) + windows: "" + + # This is the fallback script for unknown + # operating systems. + # (optional) + *: "" + ``` + +=== "Json" + + ```json + { + "setup": { + // path to a shell script or binary + // (optional) + "linux": "", + + // path to a shell script or binary + // (optional) + "darwin": "", + + // path to a executable or binary + // (optional) + "windows": "", + + // This is the fallback script for unknown + // operating systems. + // (optional) + "*": "" + } + } + ``` diff --git a/www/docs/configuration/fields/styles.md b/www/docs/configuration/fields/styles.md index 55b4716c..c27cffd3 100644 --- a/www/docs/configuration/fields/styles.md +++ b/www/docs/configuration/fields/styles.md @@ -1,7 +1,7 @@ # Styles You can specify multiple styles by using the -`styles` field in your `template.json`. +`styles` field in your `waku config`. The [setup](./setup.md), [ignore](./ignore.md), [labels](./labels.md) and [prompts](./prompts.md) fields are merged with the @@ -9,29 +9,56 @@ root-level fields. When conflicting, the values from the chosen style will take priority and overwrite the root-level fields when applicable. -## template.json - -```json -{ - "styles": { - // The key of this map represents - // the name of the style. - "My Style": { - // Source is the path to the - // directory containing the files. - // - // This has to be relative to the `template.json` - // file. - // - // (required) - "source": "", - - // These fields are optional - "setup": {}, - "ignore": [], - "labels": [], - "prompts": [], +## waku config + +=== "Yaml" + + ```yaml + styles: + my-style: + styles: + # The key of this map represents + # the name of the style. + My Style: + # Source is the path to the + # directory containing the files. + # + # This has to be relative to the `template.json` + # file. + # + # (required) + source: "" + + # These fields are optional + setup: + ignore: + labels: + prompts: + ``` + +=== "Json" + + ```json + { + "styles": { + // The key of this map represents + // the name of the style. + "My Style": { + // Source is the path to the + // directory containing the files. + // + // This has to be relative to the `template.json` + // file. + // + // (required) + "source": "", + + // These fields are optional + "setup": {}, + "ignore": [], + "labels": [], + "prompts": [], + } + } } - } -} -``` + ``` diff --git a/www/docs/configuration/introduction.md b/www/docs/configuration/introduction.md index cead3355..9f17414b 100644 --- a/www/docs/configuration/introduction.md +++ b/www/docs/configuration/introduction.md @@ -1,11 +1,11 @@ # Introduction -Styles are defined through a `template.json` file +Styles are defined through a `configuration` file usually found in the `root` of the project. ## Validation -You can validate your `template.json` file by running: +You can validate your `waku.yml` file by running: ```sh waku check @@ -20,16 +20,43 @@ for better editor support. https://waku.ngjx.org/static/schema.json ``` -Or you can pin a specific version like `v0.5.1`: +Or you can pin a specific version like `v0.6.0`: ```text -https://raw.githubusercontent.com/caffeine-addictt/waku/v0.5.1/www/docs/static/schema.json +https://raw.githubusercontent.com/caffeine-addictt/waku/v0.6.0/www/docs/static/schema.json ``` -Simply add the `$schema` property to your `template.json` file: +Simply add the `$schema` property to your `configuration` file: -```json -{ - "$schema": "https://waku.ngjx.org/static/schema.json" -} -``` +=== "Yaml" + + Add the following to your `waku.yml` file as a comment + + ```yaml + # yaml-language-server: $schema=https://waku.ngjx.org/static/schema.json + ``` + +=== "Json" + + ```json + { + "$schema": "https://waku.ngjx.org/static/schema.json" + } + ``` + +## Filenames + +Here are the default filenames we look for: + +- `waku.yml` +- `waku.yaml` +- `waku.json` +- `template.yml` +- `template.yaml` +- `template.json` +- `.waku.yml` +- `.waku.yaml` +- `.waku.json` +- `.template.yml` +- `.template.yaml` +- `.template.json` diff --git a/www/docs/configuration/quick-start.md b/www/docs/configuration/quick-start.md index 779317f3..d8f3b83c 100644 --- a/www/docs/configuration/quick-start.md +++ b/www/docs/configuration/quick-start.md @@ -18,30 +18,54 @@ let's create a new template containing 1 style called `My Style`. The files you create in this directory will be copied and formatted over when `waku new` is ran. -1. Create a `template.json` file - - Create a `template.json` file in the root or subdirectory - of your project containing the following: - - !!! WARNING - - If you use a subdirectory, - do not forget to pass the `-d|--directory ` - option when using `waku new`. - - ```json - { - "$schema": "https://waku.ngjx.org/static/schema.json", - "styles": { - "My Style": { - "source": "style-a", - "prompts": [ - { - "key": "Description", - "ask": "A brief description of your project" +1. Create a waku `config` file + + === "Yaml" + + Create a `waku.yml` file in the root or subdirectory + of your project containing the following: + + !!! WARNING + + If you use a subdirectory, + do not forget to pass the `-d|--directory ` + option when using `waku new`. + + ```yaml + # yaml-language-server: $schema=https://waku.ngjx.org/static/schema.json + + styles: + My Style: + source: style-a + prompts: + - key: Description + ask: A brief description of your project + ``` + + === "Json" + + Create a `waku.json` file in the root or subdirectory + of your project containing the following: + + !!! WARNING + + If you use a subdirectory, + do not forget to pass the `-d|--directory ` + option when using `waku new`. + + ```json + { + "$schema": "https://waku.ngjx.org/static/schema.json", + "styles": { + "My Style": { + "source": "style-a", + "prompts": [ + { + "key": "Description", + "ask": "A brief description of your project" + } + ] } - ] + } } - } - } - ``` + ``` diff --git a/www/docs/install.md b/www/docs/install.md index 4cba5580..901be985 100644 --- a/www/docs/install.md +++ b/www/docs/install.md @@ -102,17 +102,17 @@ All artifacts are checksummed, and the checksum file is signed with [cosign][]. and `checksums.txt.sig` files from the [releases][] page. ```sh - curl -O 'https://github.com/caffeine-addictt/waku/releases/download/v0.5.1/checksums.txt' + curl -O 'https://github.com/caffeine-addictt/waku/releases/download/v0.6.0/checksums.txt' ``` 1. Verify checksums signature: ```bash cosign verify-blob \ - --certificate-identity 'https://github.com/caffeine-addictt/waku/.github/workflows/release.yml@refs/tags/v0.5.1' \ + --certificate-identity 'https://github.com/caffeine-addictt/waku/.github/workflows/release.yml@refs/tags/v0.6.0' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --cert 'https://github.com/caffeine-addictt/waku/releases/download/v0.5.1/checksums.txt.pem' \ - --signature 'https://github.com/caffeine-addictt/waku/releases/download/v0.5.1/checksums.txt.sig' \ + --cert 'https://github.com/caffeine-addictt/waku/releases/download/v0.6.0/checksums.txt.pem' \ + --signature 'https://github.com/caffeine-addictt/waku/releases/download/v0.6.0/checksums.txt.sig' \ ./checksums.txt ``` @@ -130,7 +130,7 @@ Verify the signature: ```sh cosign verify \ - --certificate-identity 'https://github.com/caffeine-addictt/waku/.github/workflows/release.yml@refs/tags/v0.5.1' \ + --certificate-identity 'https://github.com/caffeine-addictt/waku/.github/workflows/release.yml@refs/tags/v0.6.0' \ --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ caffeinec/waku ``` diff --git a/www/install.md b/www/install.md deleted file mode 100644 index 878429aa..00000000 --- a/www/install.md +++ /dev/null @@ -1,150 +0,0 @@ -# Installation - -Here you will find all the ways you can install `Waku` on your system. - - - - -- [Installation](#installation) - - [AUR](#aur) - - [Golang](#golang) - - [Download](#download) - - [Snapcraft](#snapcraft) - - [Homebrew](#homebrew) - - [Scoop](#scoop) - - [Choco](#choco) - - [Docker](#docker) - - [Our images are hosted on Docker Hub and GitHub](#our-images-are-hosted-on-docker-hub-and-github) - - [Scripts](#scripts) - - [Linux/MacOS](#linuxmacos) - - [Windows](#windows) - - [Verify](#verify) - - [1. Download the checksums.txt file](#1-download-the-checksumstxt-file) - - [2. Verify checksums signature](#2-verify-checksums-signature) - - [3. Verify the SHA256 sums match](#3-verify-the-sha256-sums-match) - - - - -## AUR - -```sh -# Or with your preferred AUR helper -yay -S waku -``` - -## Golang - -```sh -go install github.com/caffeine-addictt/waku@latest -``` - -## Download - -You can also download `Waku` from our -[GitHub release page](https://github.com/caffeine-addictt/waku/releases/latest). - -## Snapcraft - -```sh -sudo snap install waku --classic -``` - -## Homebrew - -You can find the homebrew formula -[here](https://github.com/caffeine-addictt/homebrew-tap). - -```sh -brew tap caffeine-addictt/tap -brew install caffeine-addictt/tap/waku -``` - -## Scoop - -```sh -scoop bucket add caffeine https://github.com/caffeine-addictt/scoop-bucket.git -scoop install -``` - -## Choco - -```ps1 -choco install waku -``` - -## Docker - -You will need Docker to run `Waku` this way. -You can verify you have Docker installed using the command `docker --version`, -otherwise, you can install Docker from their [docs](https://docs.docker.com/get-started/get-docker/). - -### Our images are hosted on Docker Hub and GitHub - -> [!NOTE] -> At present, out images do not yet support using -> your system's Git, and thus will only be able to -> access publicly available repositories. -> -> Want to help us out? -> Check out our [Contributing Guide](https://github.com/caffeine-addictt/waku/blob/main/CONTRIBUTING.md). - -You will need to mount your current directory to `/app` in the Docker container -for `Waku` to run correctly. - -```sh -docker run -v $PWD:/app caffeinec/waku:latest -docker run -v $PWD:/app ghcr.io/caffeine-addictt/waku:latest -``` - -### Scripts - -We also provide a set of one-liners to run the `Waku` CLI from docker. - -#### Linux/MacOS - -```sh -curl -sSL https://github.com/caffeine-addictt/waku/releases/latest/download/waku.sh | bash - -# If you prefer to use Waku from GitHub -curl -sSL https://github.com/caffeine-addictt/waku/releases/latest/download/waku.sh | bash -s ghcr -``` - -#### Windows - -```ps1 -iwr -useb https://github.com/caffeine-addictt/waku/releases/latest/download/waku.ps1 | iex - -# If you prefer to use Waku from GitHub -iwr -useb https://github.com/caffeine-addictt/waku/releases/latest/download/waku.ps1 | iex; Run-Waku "ghcr" -``` - -## Verify - -### 1. Download the checksums.txt file - -Either from our [releases page](https://github.com/caffeine-addictt/waku/releases/latest) -or by running the following command: - -```sh -curl -sSL https://github.com/caffeine-addictt/waku/releases/latest/download/checksums.txt -o checksums.txt -``` - -### 2. Verify checksums signature - -We sign our checksums with [Cosign](https://github.com/sigstore/cosign) - -```sh -cosign verify-blob \ - --certificate-identity 'https://github.com/caffeine-addictt/waku/.github/workflows/release.yml@refs/tags/v0.3.1' \ - --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ - --cert 'https://github.com/caffeine-addictt/waku/releases/download/v0.3.1/checksums.txt.pem' \ - --signature 'https://github.com/caffeine-addictt/waku/releases/download/v0.3.1/checksums.txt.sig' \ - ./checksums.txt -``` - -### 3. Verify the SHA256 sums match - -```sh -sha256sum --ignore-missing -c checksums.txt -``` diff --git a/www/templating.md b/www/templating.md deleted file mode 100644 index daf39e9b..00000000 --- a/www/templating.md +++ /dev/null @@ -1,213 +0,0 @@ -# Templating - -Templating is done through the `template.json` file. -You can have it in your root or sub directory of your -repository. - -Do note that having it in a sub directory -will require the `-d|--directory` flag to be set. - - - - -- [Templating](#templating) - - [Quick start](#quick-start) - - [Creating a template](#creating-a-template) - - [Checking a template](#checking-a-template) - - [Structure](#structure) - - [Simple setup](#simple-setup) - - [Multi style setup](#multi-style-setup) - - [Fields](#fields) - - [Setup](#setup) - - [Ignore](#ignore) - - [Labels](#labels) - - [Prompts](#prompts) - - [Styles](#styles) - - [Values](#values) - - - - -## Quick start - -### Creating a template - -> [!NOTE] -> This is not supported yet. - -You can generate a style template by running `waku new template`. - -### Checking a template - -You can check if a template is valid by running `waku check`. - -## Structure - -The `template.json` can be structured in 2 ways; -i.e. `name` and `styles` is mutually exclusive. - -### Simple setup - -Here, `name` is set instead of `styles`. - -```json -{ - "$schema": "https://raw.githubusercontent.com/caffeine-addictt/waku/main/schema.json", - "name": "template name", - "setup": { - "linux": "path/to/file.sh", - "windows": "path/to/file.bat" - }, - "ignore": [ - "path/to/file", - "path/to/dir", - "path/to/files/*", - "!path/to/files/not/to/ignore" - ], - "labels": { - "my feature": "my color" - }, - "prompts": { - "my templated": "Text to prompt user with" - } -} -``` - -### Multi style setup - -The properties from `"root"` will also be applied -to each style, with each style taking higher priority. - -```json -{ - "$schema": "https://raw.githubusercontent.com/caffeine-addictt/waku/main/schema.json", - "setup": { - "linux": "path/to/file.sh", - "windows": "path/to/file.bat" - }, - "ignore": [ - "path/to/file", - "path/to/dir", - "path/to/files/*", - "!path/to/files/not/to/ignore" - ], - "labels": { - "my feature": "my color" - }, - "prompts": { - "my templated": "Text to prompt user with" - }, - "styles": { - "my style name": { - "source": "path/to/style/dir", - "setup": { - "linux": "path/to/file.sh", - "windows": "path/to/file.bat" - }, - "ignore": [ - "path/to/file", - "path/to/dir", - "path/to/files/*", - "!path/to/files/not/to/ignore" - ], - "labels": { - "my feature": "my color" - }, - "prompts": { - "my templated": "Text to prompt user with" - } - } - } -} -``` - -## Fields - -### Setup - -> [!NOTE] -> Setup is not supported yet. - -`Setup` is a `map` of `name` to `path`. -These executables will be optionally ran -after the template has been generated. - -- `Setup.name` can be `linux`, `windows`, `darwin` or `*`. - `*` is used when the user Os is unknown. -- `Setup.path` is the template-relative path to the executable. - -### Ignore - -`Ignore` is an array of path globs you do not want templated. -We only support the `*` and `!` special characters. - -- `*` is glob for everything -- `path/to/file` is a single file -- `path/to/f*` is a glob for a file -- `path/to/dir/*` is a single dir level glob -- `path/to/dir/**` == `path/to/dir/` is a recursive dir level glob -- `!path/to/file` is a negated ignore - -### Labels - -> [!NOTE] -> Labels are not supported yet. - -Labels are a `map` of `name` to `color`. -Color has to be a hex color code. - -```text -Regex -^#(?:[0-9a-fA-F]{3}){1,2}$ -``` - -### Prompts - -Prompts are a `map` of `name` to `prompt`. - -Name represent the templated value for you files. -I.e. `code` with user responding with `Go` will cause a -templated file `{{CODE}} is cool` -> `Go is cool`. - -Prompts are what users are asked with. - -### Styles - -Styles is a `map` of `name` to `style`. - -```json -{ - ..., - "styles": { - "My favorite": { - "source": "style-a", - ... - }, - "My friend's": { - "source": "style-b", - ... - }, - ... - } -} -``` - -- **`my-templates/`** - - **`template.json`** - - **`style-a/`** - - `file1` - - `file2` - - `...` - - **`style-b/`** - - `file1` - - `file2` - - `...` - - `...` - -#### Values - -- `style.source` is the path to the style directory to use. -- [`style.setup`](#setup) -- [`style.ignore`](#ignore) -- [`style.labels`](#labels) -- [`style.prompts`](#prompts)