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 }