new.go

100 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
package commands

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strings"

	"congo.gg/cmd/internal"
)

const newUsage = `Add a new app to an existing Congo project

Usage:
  congo new <name> [flags]

Flags:
  --frontend       Include React islands
  --no-database    Exclude database models

Creates a new app directory with controllers, models, views, and main.go.
Must be run from inside a Congo project (directory with go.mod).

Examples:
  congo new api
  congo new admin --frontend
  congo new worker --no-database
`

func New() {
	fs := flag.NewFlagSet("new", flag.ExitOnError)
	fs.Usage = func() { fmt.Print(newUsage) }

	withFrontend := fs.Bool("frontend", false, "Include React islands")
	noDatabase := fs.Bool("no-database", false, "Exclude database models")

	fs.Parse(os.Args[2:])

	if fs.NArg() < 1 {
		fs.Usage()
		os.Exit(1)
	}

	name := fs.Arg(0)
	if !validName.MatchString(name) {
		log.Fatalf("invalid app name %q (lowercase letters, numbers, hyphens, underscores)", name)
	}

	// Must be in a Congo project (go.mod exists).
	modData, err := os.ReadFile("go.mod")
	if err != nil {
		log.Fatal("go.mod not found. Run 'congo init <project>' first to create a project.")
	}

	// Parse module path from go.mod.
	module := parseModulePath(modData)
	if module == "" {
		log.Fatal("could not parse module path from go.mod")
	}

	if _, err := os.Stat(name); err == nil {
		log.Fatalf("directory %q already exists", name)
	}

	// Check that internal/ exists (framework already extracted).
	if _, err := os.Stat("internal"); err != nil {
		log.Fatal("internal/ not found. Run 'congo init <project>' first.")
	}

	fmt.Printf("Creating app %s...\n", name)

	data := scaffold.Data{
		Name:         name,
		Module:       module,
		Dir:          name,
		WithFrontend: *withFrontend,
		WithDatabase: !*noDatabase,
	}

	if err := scaffold.RenderApp(name, data); err != nil {
		log.Fatalf("render app: %v", err)
	}

	fmt.Printf("\nDone! App created at ./%s\n\n", name)
	fmt.Println("Next steps:")
	fmt.Printf("  go run ./%s\n", name)
	fmt.Println()
}

// parseModulePath extracts the module path from go.mod content.
func parseModulePath(data []byte) string {
	for _, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "module ") {
			return strings.TrimSpace(strings.TrimPrefix(line, "module "))
		}
	}
	return ""
}