mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-02-14 04:09:11 -06:00
- Adds options for Python and Typescript package manager support - uv, pip, poetry, npm, pnpm, yarn, and bun. Also adds e2e tests to ensure that all of these quickstarts successfully can start a worker. - Replaces the scripts with triggers in the CLI - Adds the hatchet trigger command
356 lines
10 KiB
Go
356 lines
10 KiB
Go
package cli
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/huh"
|
|
"github.com/charmbracelet/lipgloss"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/hatchet-dev/hatchet/cmd/hatchet-cli/cli/internal/config/cli"
|
|
"github.com/hatchet-dev/hatchet/cmd/hatchet-cli/cli/internal/styles"
|
|
"github.com/hatchet-dev/hatchet/cmd/hatchet-cli/cli/internal/templater"
|
|
)
|
|
|
|
//go:embed all:templates/*
|
|
var content embed.FS
|
|
|
|
var quickstartCmd = &cobra.Command{
|
|
Use: "quickstart",
|
|
Short: "Generate a quickstart Hatchet worker project",
|
|
Long: `Generate a quickstart Hatchet worker project with boilerplate code in your language of choice.
|
|
|
|
Supports multiple package managers:
|
|
Python: poetry, uv, pip
|
|
TypeScript: npm, pnpm, yarn, bun
|
|
Go: go modules`,
|
|
Example: ` # Generate a project interactively (prompts for language, package manager, name, and directory)
|
|
hatchet quickstart
|
|
|
|
# Generate a Python project with Poetry
|
|
hatchet quickstart --language python --package-manager poetry
|
|
|
|
# Generate a TypeScript project with pnpm
|
|
hatchet quickstart --language typescript --package-manager pnpm --project-name my-worker
|
|
|
|
# Generate a Python project with uv using short flags
|
|
hatchet quickstart -l python -m uv -p my-worker -d ./my-worker`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
// Check if at least one profile exists
|
|
profileNames := cli.ListProfiles()
|
|
if len(profileNames) == 0 {
|
|
fmt.Println(noProfilesMessage())
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Get flag values
|
|
language, _ := cmd.Flags().GetString("language")
|
|
packageManager, _ := cmd.Flags().GetString("package-manager")
|
|
projectName, _ := cmd.Flags().GetString("project-name")
|
|
dir, _ := cmd.Flags().GetString("directory")
|
|
|
|
// Use interactive forms only if flags not provided
|
|
if language == "" {
|
|
language = selectLanguageForm()
|
|
}
|
|
|
|
// Validate language
|
|
validLanguages := map[string]bool{"python": true, "typescript": true, "go": true}
|
|
|
|
if !validLanguages[language] {
|
|
cli.Logger.Fatalf("invalid language: %s (must be one of: python, typescript, go)", language)
|
|
}
|
|
|
|
// Get package manager
|
|
if packageManager == "" {
|
|
packageManager = selectPackageManagerForm(language)
|
|
}
|
|
|
|
// Validate package manager for the selected language
|
|
validPackageManagers := map[string]map[string]bool{
|
|
"python": {"poetry": true, "uv": true, "pip": true},
|
|
"typescript": {"npm": true, "pnpm": true, "yarn": true, "bun": true},
|
|
"go": {"go": true},
|
|
}
|
|
|
|
if !validPackageManagers[language][packageManager] {
|
|
var validOptions []string
|
|
for pm := range validPackageManagers[language] {
|
|
validOptions = append(validOptions, pm)
|
|
}
|
|
cli.Logger.Fatalf("invalid package manager '%s' for language '%s' (must be one of: %s)", packageManager, language, strings.Join(validOptions, ", "))
|
|
}
|
|
|
|
if projectName == "" {
|
|
projectName = selectNameForm()
|
|
}
|
|
|
|
if dir == "" {
|
|
dir = selectDirectoryForm(projectName)
|
|
} else {
|
|
// If directory was provided via flag, check if it exists
|
|
if _, err := os.Stat(dir); err == nil {
|
|
cli.Logger.Fatalf("directory %s already exists - please choose a different directory or delete the existing one", dir)
|
|
}
|
|
}
|
|
|
|
postQuickstart, err := GenerateQuickstart(language, packageManager, projectName, dir)
|
|
if err != nil {
|
|
cli.Logger.Fatalf("could not generate quickstart: %v", err)
|
|
}
|
|
|
|
fmt.Println(quickstartSuccessView(language, projectName, dir, postQuickstart))
|
|
},
|
|
}
|
|
|
|
// GenerateQuickstart generates a quickstart project without interactive forms.
|
|
// Returns the post-quickstart content that should be displayed to the user.
|
|
func GenerateQuickstart(language, packageManager, projectName, dir string) (string, error) {
|
|
templateData := templater.Data{
|
|
Name: projectName,
|
|
PackageManager: packageManager,
|
|
}
|
|
|
|
err := templater.ProcessMultiSource(content, language, packageManager, dir, templateData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not process templates: %w", err)
|
|
}
|
|
|
|
// Process POST_QUICKSTART.md if it exists
|
|
postQuickstart, err := templater.ProcessPostQuickstartMultiSource(content, language, packageManager, templateData)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not process post-quickstart content: %w", err)
|
|
}
|
|
|
|
return postQuickstart, nil
|
|
}
|
|
|
|
func init() {
|
|
rootCmd.AddCommand(quickstartCmd)
|
|
|
|
// Add flags for quickstart command
|
|
quickstartCmd.Flags().StringP("language", "l", "", "Programming language (python, typescript, go)")
|
|
quickstartCmd.Flags().StringP("package-manager", "m", "", "Package manager (poetry, uv, pip for Python; npm, pnpm, yarn, bun for TypeScript)")
|
|
quickstartCmd.Flags().StringP("project-name", "p", "", "Name of the project (default: hatchet-worker)")
|
|
quickstartCmd.Flags().StringP("directory", "d", "", "Directory to create the project in (default: ./{project-name})")
|
|
}
|
|
|
|
func selectLanguageForm() string {
|
|
language := ""
|
|
|
|
form := huh.NewForm(
|
|
huh.NewGroup(
|
|
huh.NewSelect[string]().
|
|
Title("Choose your language").
|
|
Options(
|
|
huh.NewOption("Python", "python"),
|
|
huh.NewOption("Typescript", "typescript"),
|
|
huh.NewOption("Go", "go"),
|
|
).
|
|
Value(&language),
|
|
),
|
|
).WithTheme(styles.HatchetTheme())
|
|
|
|
err := form.Run()
|
|
|
|
if err != nil {
|
|
cli.Logger.Fatalf("could not run quickstart form: %v", err)
|
|
}
|
|
|
|
return language
|
|
}
|
|
|
|
func selectPackageManagerForm(language string) string {
|
|
packageManager := ""
|
|
|
|
var options []huh.Option[string]
|
|
|
|
switch language {
|
|
case "python":
|
|
options = []huh.Option[string]{
|
|
huh.NewOption("Poetry (recommended)", "poetry"),
|
|
huh.NewOption("uv", "uv"),
|
|
huh.NewOption("pip", "pip"),
|
|
}
|
|
case "typescript":
|
|
options = []huh.Option[string]{
|
|
huh.NewOption("npm", "npm"),
|
|
huh.NewOption("pnpm", "pnpm"),
|
|
huh.NewOption("Yarn", "yarn"),
|
|
huh.NewOption("Bun", "bun"),
|
|
}
|
|
case "go":
|
|
// Go only has one package manager, return default
|
|
return "go"
|
|
default:
|
|
cli.Logger.Fatalf("unsupported language: %s", language)
|
|
}
|
|
|
|
form := huh.NewForm(
|
|
huh.NewGroup(
|
|
huh.NewSelect[string]().
|
|
Title("Choose your package manager").
|
|
Options(options...).
|
|
Value(&packageManager),
|
|
),
|
|
).WithTheme(styles.HatchetTheme())
|
|
|
|
err := form.Run()
|
|
|
|
if err != nil {
|
|
cli.Logger.Fatalf("could not run package manager form: %v", err)
|
|
}
|
|
|
|
return packageManager
|
|
}
|
|
|
|
func selectNameForm() string {
|
|
name := ""
|
|
|
|
form := huh.NewForm(
|
|
huh.NewGroup(
|
|
huh.NewInput().
|
|
Title("Enter a name for your project (default hatchet-worker)").
|
|
Placeholder("hatchet-worker").
|
|
Value(&name),
|
|
),
|
|
).WithTheme(styles.HatchetTheme())
|
|
|
|
err := form.Run()
|
|
|
|
if err != nil {
|
|
cli.Logger.Fatalf("could not run quickstart form: %v", err)
|
|
}
|
|
|
|
if name == "" {
|
|
name = "hatchet-worker"
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
func selectDirectoryForm(name string) string {
|
|
directory := ""
|
|
|
|
form := huh.NewForm(
|
|
huh.NewGroup(
|
|
huh.NewInput().
|
|
Title(fmt.Sprintf("Enter a directory to create your project in (default ./%s)", name)).
|
|
Placeholder(fmt.Sprintf("./%s", name)).
|
|
Value(&directory).
|
|
Validate(func(s string) error {
|
|
// Use default if empty
|
|
targetDir := s
|
|
if targetDir == "" {
|
|
targetDir = fmt.Sprintf("./%s", name)
|
|
}
|
|
|
|
// Check if directory exists
|
|
if _, err := os.Stat(targetDir); err == nil {
|
|
return fmt.Errorf("directory already exists - please choose a different name or delete the existing directory")
|
|
}
|
|
|
|
return nil
|
|
}),
|
|
),
|
|
).WithTheme(styles.HatchetTheme())
|
|
|
|
err := form.Run()
|
|
|
|
if err != nil {
|
|
cli.Logger.Fatalf("could not run quickstart form: %v", err)
|
|
}
|
|
|
|
if directory == "" {
|
|
directory = fmt.Sprintf("./%s", name)
|
|
}
|
|
|
|
return directory
|
|
}
|
|
|
|
// renderCodeBlocks processes a string and renders code blocks wrapped in ```sh
|
|
func renderCodeBlocks(content string) string {
|
|
// Simple code block rendering for ```sh blocks
|
|
lines := strings.Split(content, "\n")
|
|
var result []string
|
|
inCodeBlock := false
|
|
var codeLines []string
|
|
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "```sh") || strings.HasPrefix(line, "```bash") {
|
|
inCodeBlock = true
|
|
codeLines = []string{}
|
|
continue
|
|
}
|
|
if inCodeBlock && strings.HasPrefix(line, "```") {
|
|
// End of code block - render it
|
|
inCodeBlock = false
|
|
for _, codeLine := range codeLines {
|
|
result = append(result, styles.Code.Render(" "+codeLine))
|
|
}
|
|
continue
|
|
}
|
|
if inCodeBlock {
|
|
codeLines = append(codeLines, line)
|
|
} else {
|
|
result = append(result, line)
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, "\n")
|
|
}
|
|
|
|
// noProfilesMessage returns a formatted message when no profiles are configured
|
|
func noProfilesMessage() string {
|
|
var lines []string
|
|
|
|
headerStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.ErrorColor)
|
|
|
|
lines = append(lines, headerStyle.Render("No Hatchet profiles configured"))
|
|
lines = append(lines, "")
|
|
lines = append(lines, "To use the quickstart command, you need to connect to a Hatchet instance first.")
|
|
lines = append(lines, "")
|
|
lines = append(lines, headerStyle.Render("Option 1: Sign up for Hatchet Cloud"))
|
|
lines = append(lines, "")
|
|
lines = append(lines, "Visit "+styles.Accent.Render("https://cloud.onhatchet.run")+" to create an account, then add your profile:")
|
|
lines = append(lines, styles.Code.Render(" hatchet profile add"))
|
|
lines = append(lines, "")
|
|
lines = append(lines, headerStyle.Render("Option 2: Start a local Hatchet server"))
|
|
lines = append(lines, "")
|
|
lines = append(lines, "Run a local instance with Docker:")
|
|
lines = append(lines, styles.Code.Render(" hatchet server start"))
|
|
|
|
return styles.InfoBox.Render(strings.Join(lines, "\n"))
|
|
}
|
|
|
|
// quickstartSuccessView renders the success message with next steps
|
|
func quickstartSuccessView(language, projectName, dir, postQuickstart string) string {
|
|
var lines []string
|
|
|
|
lines = append(lines, styles.SuccessMessage(fmt.Sprintf("Successfully created %s project: %s", language, projectName)))
|
|
lines = append(lines, "")
|
|
lines = append(lines, styles.KeyValue("Location", dir))
|
|
lines = append(lines, "")
|
|
lines = append(lines, styles.Section("Next Steps"))
|
|
lines = append(lines, "")
|
|
lines = append(lines, styles.Accent.Render("1.")+" Navigate to your project:")
|
|
lines = append(lines, styles.Code.Render(" cd "+dir))
|
|
lines = append(lines, "")
|
|
lines = append(lines, styles.Accent.Render("2.")+" Start the worker in development mode:")
|
|
lines = append(lines, styles.Code.Render(" hatchet worker dev"))
|
|
|
|
// Add POST_QUICKSTART.md content as step 3 if it exists
|
|
if postQuickstart != "" {
|
|
lines = append(lines, "")
|
|
lines = append(lines, styles.Accent.Render("3.")+" "+strings.TrimSpace(renderCodeBlocks(postQuickstart)))
|
|
} else {
|
|
lines = append(lines, "")
|
|
lines = append(lines, styles.Muted.Render("The worker will automatically connect to your Hatchet instance."))
|
|
}
|
|
|
|
return styles.SuccessBox.Render(strings.Join(lines, "\n"))
|
|
}
|